Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/games/blockilization/client/concepts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export const CONCEPTS: Article[] = [
sub: 'Danger and reward in the wilds',
sections: [
{ text: 'Barbarians come out of camps in the unclaimed lands and raid whoever is near. Clearing a camp brings gold, and city-states close by are grateful for it.' },
{ title: 'Captured workers', text: 'A worker (or a settler, which becomes a worker) that barbarians catch is theirs, and is led back to their camp to wait there. Step a soldier onto it on the way, or clear the camp, to take it back.' },
{ title: 'Clans', text: 'Every camp is a clan’s, and a civilization that has found a camp can deal with its clan in the Diplomacy screen (or by clicking the camp): bribe it, and its raiders leave you and your team alone for a while; hire one of its warriors, now and then; or pay it to raid a civilization you have met, which will hear of it, and may find out who paid. Left alone, a clan grows, faster when paid and as its raiders pillage, and slower as they die. Grown, it settles down where its camp is as a new city-state, if the camp is in no one’s land and no city is close by.' },
{ text: 'Ancient ruins are scattered over the world. The first land unit to step on one gets its reward: gold, a technology, a citizen, a map of the land round it, or a unit.' },
],
Expand Down
2 changes: 2 additions & 0 deletions src/games/blockilization/client/models3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ export function drawModels(client: Client, dt: number) {
units.set(u.id, s);
}
s.seen = frame;
// (Whose it is now: a worker captured is the captor's, and isn't seen to die when it walks off out of sight.)
s.owner = u.owner;
// Moved: it marches there, tile by tile.
if (s.tile !== t) {
march(s, u, s.tile, t, tx, tz, client.time);
Expand Down
33 changes: 27 additions & 6 deletions src/games/blockilization/rules/barbarians.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ import { campCleared } from './quests';
import { clanAt, clanById, clanPillaged, clansTurn, endClan, newClan, raidTarget, spares } from './clans';
import type { Game, Unit } from './state';
import { TERRAINS } from './terrain';
import { UNITS } from './units';
import { isMilitary, UNITS } from './units';

/**
* Barbarians, Civ V's way: camps appear in land nobody can see, each sends out a raider every so
* often (stronger as the world's techs advance), raiders go for whatever's near (units, workers,
* cities) and pillage what they stand on, and whoever clears a camp takes 25 gold (Germany, often a
* raider who joins them too). They never take a city.
* raider who joins them too). They never take a city. A worker (or settler) they catch is theirs, a
* worker led back to the camp, where it waits for anyone to take it back (Civ V's).
*
* Each camp is a clan's (`clans.ts`): its raiders leave alone the civs that have bribed it, and go
* after the one it's been paid to raid, more of them, and farther.
*/

export const barbarianIndex = (game: Game) => game.players.findIndex((p) => p.civ === 'barbarian');

/** The barbarians' fighting units (not the workers they've taken). */
const raiders = (game: Game, b: number) => unitsOf(game, b).filter((u) => isMilitary(UNITS[u.type]));

/** The unit a camp sends out: by the most advanced civ's era. */
function raider(game: Game): string {
const most = Math.max(...game.players.filter((p) => p.civ !== 'barbarian').map((p) => p.techs.length));
Expand Down Expand Up @@ -56,16 +60,33 @@ export function barbarianTurn(game: Game) {
const paid = raidTarget(game, clan) !== null;
if (random(game) > (paid ? 0.2 : 0.06)) return;
const near = within(game.grid, t, 1).find((x) => canEnter(game, { id: -1, type: 'warrior', owner: b, tile: x, moves: 0, hp: 100, xp: 0, promotions: [] }, x));
const around = game.units.filter((x) => x.owner === b && distance(game.grid, x.tile, t) <= 4).length;
if (near !== undefined && around < (paid ? 4 : 2) && (paid || unitsOf(game, b).length < maxCamps * 2)) spawnUnit(game, raider(game), b, near, { clan: clan?.id });
const around = raiders(game, b).filter((x) => distance(game.grid, x.tile, t) <= 4).length;
if (near !== undefined && around < (paid ? 4 : 2) && (paid || raiders(game, b).length < maxCamps * 2)) spawnUnit(game, raider(game), b, near, { clan: clan?.id });
});
// Raiders: attack what's next to them, else head for something within reach, else wander.
// Raiders: attack what's next to them, else head for something within reach, else wander. The
// workers they've taken: back to the camp.
for (const u of unitsOf(game, b)) {
if (!game.units.includes(u)) continue;
raid(game, u, b);
if (isMilitary(UNITS[u.type])) raid(game, u, b);
else homeward(game, u);
}
}

/**
* A worker the barbarians have taken, led back to its captors' camp (or, theirs gone, the nearest),
* to wait there: beside it, if another waits in it already. With no camp left, it waits where it is.
*/
function homeward(game: Game, u: Unit) {
const clan = clanById(game, u.clan);
const camps = game.tiles.flatMap((tile, t) => (tile.camp ? [t] : [])).sort((a, c) => distance(game.grid, a, u.tile) - distance(game.grid, c, u.tile));
const camp = clan && game.tiles[clan.tile]?.camp ? clan.tile : camps[0];
u.path = undefined;
if (camp === undefined || u.tile === camp) return;
const to = [camp, ...neighbours(game.grid, camp).sort((a, c) => distance(game.grid, a, u.tile) - distance(game.grid, c, u.tile))].find((t) => t === u.tile || canEnter(game, u, t));
if (to === undefined || to === u.tile) return;
u.path = planPath(game, u, to) ?? undefined;
}

function raid(game: Game, u: Unit, b: number) {
const type = UNITS[u.type];
// (Its clan's: whom it spares, a bribe's or a raid paid for, and whom it's been paid to raid.)
Expand Down
18 changes: 15 additions & 3 deletions src/games/blockilization/rules/combat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,22 +450,34 @@ export function die(game: Game, u: Unit, by: Unit | null, how?: string) {
cityHooks.killed(game, u, by);
}

/** A civilian taken by a military unit: settlers become workers (Civ V's), workers change hands. */
/** Whether an enemy's civilian of this type is taken when caught (workers, settlers), not killed (great people, missionaries, caravans). */
export const takenAlive = (type: string) => !isGreatPerson(type) && !['missionary', 'inquisitor', 'caravan', 'cargo_ship'].includes(type);

/**
* A civilian taken by a military unit: settlers become workers (Civ V's), workers change hands. One
* the barbarians take is led back to their camp (`barbarians.ts`), for anyone to take back on the
* way there or by clearing the camp.
*/
export function capture(game: Game, by: Unit, civ: Unit) {
const name = UNITS[civ.type].name;
if (!takenAlive(civ.type)) {
removeUnit(game, civ);
note(game, civ.owner, { text: `Your ${UNITS[civ.type].name} was killed by ${game.players[by.owner].name}'s ${UNITS[by.type].name}.`, at: civ.tile, kind: 'bad' });
note(game, civ.owner, { text: `Your ${name} was killed by ${game.players[by.owner].name}'s ${UNITS[by.type].name}.`, at: civ.tile, kind: 'bad' });
return;
}
note(game, civ.owner, { text: `Your ${UNITS[civ.type].name} was captured.`, at: civ.tile, kind: 'bad' });
const barbarians = game.players[by.owner].civ === 'barbarian';
note(game, civ.owner, { text: barbarians ? `Your ${name} was captured by barbarians, who are taking it back to their camp: catch it on the way, or clear the camp, to free it.` : `Your ${name} was captured.`, at: civ.tile, kind: 'bad' });
if (game.players[civ.owner].civ === 'barbarian') note(game, by.owner, { text: `You have freed a ${name} from the barbarians.`, at: civ.tile, kind: 'good' });
civ.owner = by.owner;
civ.type = civ.type === 'settler' ? 'worker' : civ.type;
// (Taken by a clan's raider, it's that clan's, and goes to its camp; taken from them, no one's.)
civ.clan = by.clan;
civ.moves = 0;
civ.path = undefined;
civ.work = undefined;
civ.auto = undefined;
civ.stance = undefined;
civ.site = undefined;
if (TERRAINS[game.tiles[civ.tile].terrain].water) removeUnit(game, civ);
}

Expand Down
37 changes: 37 additions & 0 deletions src/games/blockilization/tests/cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6354,6 +6354,43 @@ test('clans: one left alone grows into a city-state where its camp was, its raid
ok(clansOf(game).some((x) => x.tile === camp3), 'an old camp given a clan');
});

test('barbarians: a worker they take is led back to its captors’ camp (not into a fight), and taken back on the way or by clearing the camp', () => {
const { game, at } = blank(['egypt', 'babylon'], { barbarians: true });
const [egypt] = game.players;
egypt.bot = false;
const b = game.players.findIndex((x) => x.civ === 'barbarian');
const camp = at(18, 10);
game.tiles[camp].camp = true;
const clan = newClan(game, camp);
city(game, 0, at(6, 10));
// (A soldier of Egypt's by its land: the worker, taken, once went for it as a raider would, and died.)
unit(game, 'warrior', 0, at(9, 12), { stance: 'fortify' });
const worker = unit(game, 'worker', 0, at(11, 10), { auto: 'work' });
unit(game, 'warrior', b, at(12, 10), { clan: clan.id });
barbarianTurn(game);
ok(game.units.includes(worker) && worker.owner === b, 'the worker is the barbarians’');
ok(worker.clan === clan.id && !worker.auto, 'its captors’ clan’s, and no longer at work for Egypt');
ok(egypt.notes.some((n) => n.text.startsWith('Your Worker was captured by barbarians, who are taking it back to their camp')), 'Egypt is told where it’s taken');
for (let i = 0; i < 10 && game.units.includes(worker) && worker.tile !== camp; i++) endTurn(game);
ok(game.units.includes(worker) && worker.owner === b, 'it’s still there, the barbarians’');
eq(worker.tile, camp, 'it has walked back to the camp');
endTurn(game);
eq(worker.tile, camp, 'and waits there');
// Caught on the way: Egypt's again.
const captive = unit(game, 'worker', b, at(10, 4), { clan: clan.id });
const rescuer = unit(game, 'warrior', 0, at(9, 4));
egypt.notes = [];
ok(apply(game, 0, { t: 'move', unit: rescuer.id, to: captive.tile }).ok, 'stepped on');
ok(captive.owner === 0 && captive.clan === undefined, 'taken back, no clan’s');
ok(egypt.notes.some((n) => n.text === 'You have freed a Worker from the barbarians.'), 'Egypt is told');
// The camp cleared: the worker in it Egypt's.
const defender = unit(game, 'warrior', b, camp, { clan: clan.id, hp: 1 });
const attacker = unit(game, 'swordsman', 0, neighbours(game.grid, camp)[0]);
ok(apply(game, 0, { t: 'attack', unit: attacker.id, target: camp }).ok, 'the camp attacked');
ok(!game.units.includes(defender) && attacker.tile === camp && !game.tiles[camp].camp, 'its defender killed, the camp cleared');
eq(worker.owner, 0, 'the worker in it taken back');
});

test('clans: a bot bribes a clan by its cities, pays one near its enemy to raid it, and hires a warrior at war', () => {
const { game, at } = blank(['egypt', 'babylon'], { barbarians: true });
const [egypt] = game.players;
Expand Down
Loading