diff --git a/src/games/blockilization/client/concepts.ts b/src/games/blockilization/client/concepts.ts index 29d698a..ef006b1 100644 --- a/src/games/blockilization/client/concepts.ts +++ b/src/games/blockilization/client/concepts.ts @@ -359,6 +359,10 @@ export const ABILITY_TEXT: Record = { discover_tech: 'Discovers science toward your research.', kill_heal: 'Heals 25 when it kills a unit.', jungle_fighter: '+33% strength fighting in forest and jungle.', + homeland: '+25% strength while it stands in your own land.', + foreign_lands: '+20% strength while it stands outside your own land.', + haka: 'Enemy units next to it fight at -10% strength.', + amphibious: 'No penalty for attacking from the sea or across a river.', political_treatise: 'Writes a political treatise: culture toward your next policy.', withdraw: 'May attack and still move.', }; diff --git a/src/games/blockilization/client/icons.ts b/src/games/blockilization/client/icons.ts index 79876df..56daf55 100644 --- a/src/games/blockilization/client/icons.ts +++ b/src/games/blockilization/client/icons.ts @@ -280,6 +280,22 @@ const G: Record = { tulip: ``, shield_spears: ``, chakana: ``, + star: ``, + castile: ``, + chedi: ``, + obelisk: ``, + triskele: [0, 120, 240].map((a) => turn(a, '')).join(''), + tanit: ``, + wing: '', + windmill: ``, + moai: ``, + longship: ``, + tricorne: ``, + morion: ``, + gasha: ``, + targe: ``, + patu: turn(-45, ``), + bearded_axe: ``, skull: ``, anchor_f: ``, lyre_f: ``, @@ -441,10 +457,12 @@ const UNIT_GLYPH: Record = { scout: 'footprints', explorer: 'compass', warrior: 'club', jaguar: 'jaguar', spearman: 'spear', immortal: 'spear_shield', pikeman: 'pikes', landsknecht: 'zweihander', swordsman: 'gladius', mohawk_warrior: 'tomahawk', legion: 'scutum', longswordsman: 'longsword', samurai: 'katana', hoplite: 'aspis', impi: 'isihlangu', + pictish_warrior: 'targe', maori_warrior: 'patu', berserker: 'bearded_axe', archer: 'bow', slinger: 'sling', composite_bowman: 'recurve', crossbowman: 'crossbow', longbowman: 'longbow', gatling_gun: 'gatling', machine_gun: 'machine_gun', chariot_archer: 'chariot', war_elephant: 'elephant', horseman: 'horse_head', knight: 'helm', camel_archer: 'camel', keshik: 'horse_bow', lancer: 'lance_flag', cavalry: 'horseshoe', + conquistador: 'morion', naresuans_elephant: 'elephant', winged_hussar: 'wing', catapult: 'catapult', trebuchet: 'trebuchet', cannon: 'cannon', artillery: 'artillery', rocket_artillery: 'rocket_truck', - musketman: 'musket', janissary: 'bork', rifleman: 'rifle', great_war_infantry: 'brodie', infantry: 'helmet', paratrooper: 'parachute', mechanized_infantry: 'apc', xcom_squad: 'visor', + musketman: 'musket', janissary: 'bork', minuteman: 'tricorne', rifleman: 'rifle', mehal_sefari: 'gasha', great_war_infantry: 'brodie', infantry: 'helmet', paratrooper: 'parachute', mechanized_infantry: 'apc', xcom_squad: 'visor', anti_tank_gun: 'at_gun', anti_aircraft_gun: 'aa_gun', mobile_sam: 'sam', bazooka: 'bazooka', landship: 'landship', tank: 'tank', modern_armor: 'modern_tank', giant_death_robot: 'mech', helicopter_gunship: 'helicopter', triplane: 'triplane', fighter: 'prop_plane', jet_fighter: 'jet', great_war_bomber: 'biplane', bomber: 'heavy_bomber', stealth_bomber: 'flying_wing', @@ -466,7 +484,8 @@ export function unitGlyph(type: string): string { export const CIV_SYMBOL: Record = { egypt: 'ankh', babylon: 'ziggurat', china: 'pagoda', india: 'chakra', persia: 'wingsun', korea: 'taeguk', arabia: 'crescent', germany: 'eagle', iroquois: 'pine', aztec: 'sunstone', russia: 'onion', france: 'fleur', rome: 'laurel', greece: 'corinthian', japan: 'torii', england: 'tudor', - mongolia: 'soyombo', ottomans: 'tulip', zulu: 'shield_spears', inca: 'chakana', barbarian: 'skull', + mongolia: 'soyombo', ottomans: 'tulip', zulu: 'shield_spears', inca: 'chakana', america: 'star', spain: 'castile', siam: 'chedi', ethiopia: 'obelisk', + celts: 'triskele', carthage: 'tanit', poland: 'wing', netherlands: 'windmill', polynesia: 'moai', denmark: 'longship', barbarian: 'skull', }; const CS_SYMBOL: Record = { maritime: 'anchor_f', cultured: 'lyre_f', militaristic: 'swords_f', mercantile: 'coin_f', religious: 'temple_f' }; diff --git a/src/games/blockilization/client/models3d.ts b/src/games/blockilization/client/models3d.ts index b331e48..3038dd2 100644 --- a/src/games/blockilization/client/models3d.ts +++ b/src/games/blockilization/client/models3d.ts @@ -53,6 +53,8 @@ const BY_TYPE: Partial> = { landship: 'u_landship', tank: 'u_tank', modern_armor: 'u_armor', giant_death_robot: 'u_robot', helicopter_gunship: 'u_heli', triplane: 'u_triplane', great_war_bomber: 'u_oldbomber', fighter: 'u_fighter', jet_fighter: 'u_fighter', bomber: 'u_bomber', stealth_bomber: 'u_stealth', guided_missile: 'u_missile', nuclear_missile: 'u_nuke', atomic_bomb: 'u_nuke', legion: 'u_sword', hoplite: 'u_spear', samurai: 'u_longsword', longbowman: 'u_bow', keshik: 'u_horse', janissary: 'u_musket', impi: 'u_spear', slinger: 'u_bow', + minuteman: 'u_musket', conquistador: 'u_knight', naresuans_elephant: 'u_elephant', mehal_sefari: 'u_rifle', pictish_warrior: 'u_spear', winged_hussar: 'u_cavalry', maori_warrior: 'u_warrior', berserker: 'u_longsword', + quinquereme: 'u_galley', sea_beggar: 'u_sail', trireme: 'u_galley', galleass: 'u_galley', turtle_ship: 'u_turtle', caravel: 'u_sail', frigate: 'u_sail', privateer: 'u_sail', ironclad: 'u_ironclad', destroyer: 'u_warship', battleship: 'u_warship', missile_cruiser: 'u_warship', submarine: 'u_sub', nuclear_submarine: 'u_sub', carrier: 'u_carrier', }; diff --git a/src/games/blockilization/client/pedia.ts b/src/games/blockilization/client/pedia.ts index 0d3f767..53dc456 100644 --- a/src/games/blockilization/client/pedia.ts +++ b/src/games/blockilization/client/pedia.ts @@ -260,6 +260,7 @@ const WHEN: Record = { vs_cities: 'against cities', vs_barbarians: 'against barbarians', vs_city_states: 'against city-states', + vs_more_cities: 'against civs with more cities', beside_friend: 'beside another of your units', faithful_land: 'in your land around cities of your religion', faithful_enemy_land: "in enemy land around cities of your religion", diff --git a/src/games/blockilization/client/picturefiles.ts b/src/games/blockilization/client/picturefiles.ts index e1786f6..d49c4f0 100644 --- a/src/games/blockilization/client/picturefiles.ts +++ b/src/games/blockilization/client/picturefiles.ts @@ -113,6 +113,36 @@ import l_zulu_cross from '../pictures/l_zulu_cross.png?url'; import l_inca from '../pictures/l_inca.png?url'; import l_inca_glad from '../pictures/l_inca_glad.png?url'; import l_inca_cross from '../pictures/l_inca_cross.png?url'; +import l_america from '../pictures/l_america.png?url'; +import l_america_glad from '../pictures/l_america_glad.png?url'; +import l_america_cross from '../pictures/l_america_cross.png?url'; +import l_spain from '../pictures/l_spain.png?url'; +import l_spain_glad from '../pictures/l_spain_glad.png?url'; +import l_spain_cross from '../pictures/l_spain_cross.png?url'; +import l_siam from '../pictures/l_siam.png?url'; +import l_siam_glad from '../pictures/l_siam_glad.png?url'; +import l_siam_cross from '../pictures/l_siam_cross.png?url'; +import l_ethiopia from '../pictures/l_ethiopia.png?url'; +import l_ethiopia_glad from '../pictures/l_ethiopia_glad.png?url'; +import l_ethiopia_cross from '../pictures/l_ethiopia_cross.png?url'; +import l_celts from '../pictures/l_celts.png?url'; +import l_celts_glad from '../pictures/l_celts_glad.png?url'; +import l_celts_cross from '../pictures/l_celts_cross.png?url'; +import l_carthage from '../pictures/l_carthage.png?url'; +import l_carthage_glad from '../pictures/l_carthage_glad.png?url'; +import l_carthage_cross from '../pictures/l_carthage_cross.png?url'; +import l_poland from '../pictures/l_poland.png?url'; +import l_poland_glad from '../pictures/l_poland_glad.png?url'; +import l_poland_cross from '../pictures/l_poland_cross.png?url'; +import l_netherlands from '../pictures/l_netherlands.png?url'; +import l_netherlands_glad from '../pictures/l_netherlands_glad.png?url'; +import l_netherlands_cross from '../pictures/l_netherlands_cross.png?url'; +import l_polynesia from '../pictures/l_polynesia.png?url'; +import l_polynesia_glad from '../pictures/l_polynesia_glad.png?url'; +import l_polynesia_cross from '../pictures/l_polynesia_cross.png?url'; +import l_denmark from '../pictures/l_denmark.png?url'; +import l_denmark_glad from '../pictures/l_denmark_glad.png?url'; +import l_denmark_cross from '../pictures/l_denmark_cross.png?url'; import w_pyramid from '../pictures/w_pyramid.png?url'; import w_henge from '../pictures/w_henge.png?url'; import w_colossus from '../pictures/w_colossus.png?url'; @@ -233,6 +263,16 @@ import l_mongolia_sheet from '../pictures/l_mongolia_sheet.png?url'; import l_ottomans_sheet from '../pictures/l_ottomans_sheet.png?url'; import l_zulu_sheet from '../pictures/l_zulu_sheet.png?url'; import l_inca_sheet from '../pictures/l_inca_sheet.png?url'; +import l_america_sheet from '../pictures/l_america_sheet.png?url'; +import l_spain_sheet from '../pictures/l_spain_sheet.png?url'; +import l_siam_sheet from '../pictures/l_siam_sheet.png?url'; +import l_ethiopia_sheet from '../pictures/l_ethiopia_sheet.png?url'; +import l_celts_sheet from '../pictures/l_celts_sheet.png?url'; +import l_carthage_sheet from '../pictures/l_carthage_sheet.png?url'; +import l_poland_sheet from '../pictures/l_poland_sheet.png?url'; +import l_netherlands_sheet from '../pictures/l_netherlands_sheet.png?url'; +import l_polynesia_sheet from '../pictures/l_polynesia_sheet.png?url'; +import l_denmark_sheet from '../pictures/l_denmark_sheet.png?url'; -export const PICTURES = { u_warrior, u_spear, u_pike, u_sword, u_longsword, u_bow, u_crossbow, u_musket, u_rifle, u_infantry, u_mech, u_scout, u_worker, u_missionary, u_inquisitor, u_prophet, u_great, u_horse, u_knight, u_cavalry, u_elephant, u_camel, u_general, u_chariot, u_catapult, u_trebuchet, u_cannon, u_artillery, u_rocket, u_gatling, u_landship, u_tank, u_armor, u_robot, u_heli, u_triplane, u_oldbomber, u_fighter, u_bomber, u_stealth, u_missile, u_nuke, u_galley, u_turtle, u_sail, u_ironclad, u_warship, u_sub, u_carrier, u_cargo, u_boat, u_transport, u_settler, u_caravan, l_egypt, l_egypt_glad, l_egypt_cross, l_babylon, l_babylon_glad, l_babylon_cross, l_china, l_china_glad, l_china_cross, l_india, l_india_glad, l_india_cross, l_persia, l_persia_glad, l_persia_cross, l_korea, l_korea_glad, l_korea_cross, l_arabia, l_arabia_glad, l_arabia_cross, l_germany, l_germany_glad, l_germany_cross, l_iroquois, l_iroquois_glad, l_iroquois_cross, l_aztec, l_aztec_glad, l_aztec_cross, l_russia, l_russia_glad, l_russia_cross, l_france, l_france_glad, l_france_cross, l_rome, l_rome_glad, l_rome_cross, l_greece, l_greece_glad, l_greece_cross, l_japan, l_japan_glad, l_japan_cross, l_england, l_england_glad, l_england_cross, l_mongolia, l_mongolia_glad, l_mongolia_cross, l_ottomans, l_ottomans_glad, l_ottomans_cross, l_zulu, l_zulu_glad, l_zulu_cross, l_inca, l_inca_glad, l_inca_cross, w_pyramid, w_henge, w_colossus, w_liberty, w_cristo, w_zeus, w_lighthouse, w_gardens, w_temple, w_palace, w_wall, w_arena, w_dome, w_steps, w_cathedral, w_castle, w_pagoda, w_clock, w_spire, w_gate, w_theatre, w_opera, w_modern, w_dish, w_army, w_rocket, w_atom, w_mausoleum, w_oracle, w_parthenon, w_great_mosque, w_alhambra, w_taj_mahal, w_borobudur, w_angkor_wat, w_machu_picchu, w_great_library, w_forbidden_palace, w_uffizi, w_louvre, w_sistine_chapel, w_leaning_tower, w_himeji_castle, w_broadway, w_cn_tower, w_petra, w_united_nations, w_pentagon, w_great_firewall, w_kremlin, w_pyramid_turn, w_henge_turn, w_colossus_turn, w_liberty_turn, w_cristo_turn, w_zeus_turn, w_lighthouse_turn, w_gardens_turn, w_temple_turn, w_palace_turn, w_wall_turn, w_arena_turn, w_dome_turn, w_steps_turn, w_cathedral_turn, w_castle_turn, w_pagoda_turn, w_clock_turn, w_spire_turn, w_gate_turn, w_theatre_turn, w_opera_turn, w_modern_turn, w_dish_turn, w_army_turn, w_rocket_turn, w_atom_turn, w_mausoleum_turn, w_oracle_turn, w_parthenon_turn, w_great_mosque_turn, w_alhambra_turn, w_taj_mahal_turn, w_borobudur_turn, w_angkor_wat_turn, w_machu_picchu_turn, w_great_library_turn, w_forbidden_palace_turn, w_uffizi_turn, w_louvre_turn, w_sistine_chapel_turn, w_leaning_tower_turn, w_himeji_castle_turn, w_broadway_turn, w_cn_tower_turn, w_petra_turn, w_united_nations_turn, w_pentagon_turn, w_great_firewall_turn, w_kremlin_turn, l_egypt_sheet, l_babylon_sheet, l_china_sheet, l_india_sheet, l_persia_sheet, l_korea_sheet, l_arabia_sheet, l_germany_sheet, l_iroquois_sheet, l_aztec_sheet, l_russia_sheet, l_france_sheet, l_rome_sheet, l_greece_sheet, l_japan_sheet, l_england_sheet, l_mongolia_sheet, l_ottomans_sheet, l_zulu_sheet, l_inca_sheet } as const; +export const PICTURES = { u_warrior, u_spear, u_pike, u_sword, u_longsword, u_bow, u_crossbow, u_musket, u_rifle, u_infantry, u_mech, u_scout, u_worker, u_missionary, u_inquisitor, u_prophet, u_great, u_horse, u_knight, u_cavalry, u_elephant, u_camel, u_general, u_chariot, u_catapult, u_trebuchet, u_cannon, u_artillery, u_rocket, u_gatling, u_landship, u_tank, u_armor, u_robot, u_heli, u_triplane, u_oldbomber, u_fighter, u_bomber, u_stealth, u_missile, u_nuke, u_galley, u_turtle, u_sail, u_ironclad, u_warship, u_sub, u_carrier, u_cargo, u_boat, u_transport, u_settler, u_caravan, l_egypt, l_egypt_glad, l_egypt_cross, l_babylon, l_babylon_glad, l_babylon_cross, l_china, l_china_glad, l_china_cross, l_india, l_india_glad, l_india_cross, l_persia, l_persia_glad, l_persia_cross, l_korea, l_korea_glad, l_korea_cross, l_arabia, l_arabia_glad, l_arabia_cross, l_germany, l_germany_glad, l_germany_cross, l_iroquois, l_iroquois_glad, l_iroquois_cross, l_aztec, l_aztec_glad, l_aztec_cross, l_russia, l_russia_glad, l_russia_cross, l_france, l_france_glad, l_france_cross, l_rome, l_rome_glad, l_rome_cross, l_greece, l_greece_glad, l_greece_cross, l_japan, l_japan_glad, l_japan_cross, l_england, l_england_glad, l_england_cross, l_mongolia, l_mongolia_glad, l_mongolia_cross, l_ottomans, l_ottomans_glad, l_ottomans_cross, l_zulu, l_zulu_glad, l_zulu_cross, l_inca, l_inca_glad, l_inca_cross, l_america, l_america_glad, l_america_cross, l_spain, l_spain_glad, l_spain_cross, l_siam, l_siam_glad, l_siam_cross, l_ethiopia, l_ethiopia_glad, l_ethiopia_cross, l_celts, l_celts_glad, l_celts_cross, l_carthage, l_carthage_glad, l_carthage_cross, l_poland, l_poland_glad, l_poland_cross, l_netherlands, l_netherlands_glad, l_netherlands_cross, l_polynesia, l_polynesia_glad, l_polynesia_cross, l_denmark, l_denmark_glad, l_denmark_cross, w_pyramid, w_henge, w_colossus, w_liberty, w_cristo, w_zeus, w_lighthouse, w_gardens, w_temple, w_palace, w_wall, w_arena, w_dome, w_steps, w_cathedral, w_castle, w_pagoda, w_clock, w_spire, w_gate, w_theatre, w_opera, w_modern, w_dish, w_army, w_rocket, w_atom, w_mausoleum, w_oracle, w_parthenon, w_great_mosque, w_alhambra, w_taj_mahal, w_borobudur, w_angkor_wat, w_machu_picchu, w_great_library, w_forbidden_palace, w_uffizi, w_louvre, w_sistine_chapel, w_leaning_tower, w_himeji_castle, w_broadway, w_cn_tower, w_petra, w_united_nations, w_pentagon, w_great_firewall, w_kremlin, w_pyramid_turn, w_henge_turn, w_colossus_turn, w_liberty_turn, w_cristo_turn, w_zeus_turn, w_lighthouse_turn, w_gardens_turn, w_temple_turn, w_palace_turn, w_wall_turn, w_arena_turn, w_dome_turn, w_steps_turn, w_cathedral_turn, w_castle_turn, w_pagoda_turn, w_clock_turn, w_spire_turn, w_gate_turn, w_theatre_turn, w_opera_turn, w_modern_turn, w_dish_turn, w_army_turn, w_rocket_turn, w_atom_turn, w_mausoleum_turn, w_oracle_turn, w_parthenon_turn, w_great_mosque_turn, w_alhambra_turn, w_taj_mahal_turn, w_borobudur_turn, w_angkor_wat_turn, w_machu_picchu_turn, w_great_library_turn, w_forbidden_palace_turn, w_uffizi_turn, w_louvre_turn, w_sistine_chapel_turn, w_leaning_tower_turn, w_himeji_castle_turn, w_broadway_turn, w_cn_tower_turn, w_petra_turn, w_united_nations_turn, w_pentagon_turn, w_great_firewall_turn, w_kremlin_turn, l_egypt_sheet, l_babylon_sheet, l_china_sheet, l_india_sheet, l_persia_sheet, l_korea_sheet, l_arabia_sheet, l_germany_sheet, l_iroquois_sheet, l_aztec_sheet, l_russia_sheet, l_france_sheet, l_rome_sheet, l_greece_sheet, l_japan_sheet, l_england_sheet, l_mongolia_sheet, l_ottomans_sheet, l_zulu_sheet, l_inca_sheet, l_america_sheet, l_spain_sheet, l_siam_sheet, l_ethiopia_sheet, l_celts_sheet, l_carthage_sheet, l_poland_sheet, l_netherlands_sheet, l_polynesia_sheet, l_denmark_sheet } as const; export type PictureId = keyof typeof PICTURES; diff --git a/src/games/blockilization/meta.ts b/src/games/blockilization/meta.ts index 74da9a5..9623f2c 100644 --- a/src/games/blockilization/meta.ts +++ b/src/games/blockilization/meta.ts @@ -70,5 +70,15 @@ export default defineMeta({ win_ottomans: { title: 'The Magnificent', description: 'Win as the Ottomans' }, win_zulu: { title: 'People of the Heavens', description: 'Win as the Zulu' }, win_inca: { title: 'Children of the Sun', description: 'Win as the Inca' }, + win_america: { title: 'E Pluribus Unum', description: 'Win as America' }, + win_spain: { title: 'Plus Ultra', description: 'Win as Spain' }, + win_siam: { title: 'Dawn of Happiness', description: 'Win as Siam' }, + win_ethiopia: { title: 'Land of Origins', description: 'Win as Ethiopia' }, + win_celts: { title: 'The Old Ways', description: 'Win as the Celts' }, + win_carthage: { title: 'The New City', description: 'Win as Carthage' }, + win_poland: { title: 'The Golden Freedom', description: 'Win as Poland' }, + win_netherlands: { title: 'Je Maintiendrai', description: 'Win as the Netherlands' }, + win_polynesia: { title: 'The Great Navigators', description: 'Win as Polynesia' }, + win_denmark: { title: 'Kings of the North Sea', description: 'Win as Denmark' }, }, }); diff --git a/src/games/blockilization/pictures/l_america.png b/src/games/blockilization/pictures/l_america.png new file mode 100644 index 0000000..9191e66 Binary files /dev/null and b/src/games/blockilization/pictures/l_america.png differ diff --git a/src/games/blockilization/pictures/l_america_cross.png b/src/games/blockilization/pictures/l_america_cross.png new file mode 100644 index 0000000..7ee749c Binary files /dev/null and b/src/games/blockilization/pictures/l_america_cross.png differ diff --git a/src/games/blockilization/pictures/l_america_glad.png b/src/games/blockilization/pictures/l_america_glad.png new file mode 100644 index 0000000..0599ddd Binary files /dev/null and b/src/games/blockilization/pictures/l_america_glad.png differ diff --git a/src/games/blockilization/pictures/l_america_sheet.png b/src/games/blockilization/pictures/l_america_sheet.png new file mode 100644 index 0000000..6672e5d Binary files /dev/null and b/src/games/blockilization/pictures/l_america_sheet.png differ diff --git a/src/games/blockilization/pictures/l_carthage.png b/src/games/blockilization/pictures/l_carthage.png new file mode 100644 index 0000000..dd187cc Binary files /dev/null and b/src/games/blockilization/pictures/l_carthage.png differ diff --git a/src/games/blockilization/pictures/l_carthage_cross.png b/src/games/blockilization/pictures/l_carthage_cross.png new file mode 100644 index 0000000..be4ccad Binary files /dev/null and b/src/games/blockilization/pictures/l_carthage_cross.png differ diff --git a/src/games/blockilization/pictures/l_carthage_glad.png b/src/games/blockilization/pictures/l_carthage_glad.png new file mode 100644 index 0000000..458ab24 Binary files /dev/null and b/src/games/blockilization/pictures/l_carthage_glad.png differ diff --git a/src/games/blockilization/pictures/l_carthage_sheet.png b/src/games/blockilization/pictures/l_carthage_sheet.png new file mode 100644 index 0000000..3069e51 Binary files /dev/null and b/src/games/blockilization/pictures/l_carthage_sheet.png differ diff --git a/src/games/blockilization/pictures/l_celts.png b/src/games/blockilization/pictures/l_celts.png new file mode 100644 index 0000000..1e68ce4 Binary files /dev/null and b/src/games/blockilization/pictures/l_celts.png differ diff --git a/src/games/blockilization/pictures/l_celts_cross.png b/src/games/blockilization/pictures/l_celts_cross.png new file mode 100644 index 0000000..b04ad94 Binary files /dev/null and b/src/games/blockilization/pictures/l_celts_cross.png differ diff --git a/src/games/blockilization/pictures/l_celts_glad.png b/src/games/blockilization/pictures/l_celts_glad.png new file mode 100644 index 0000000..e688691 Binary files /dev/null and b/src/games/blockilization/pictures/l_celts_glad.png differ diff --git a/src/games/blockilization/pictures/l_celts_sheet.png b/src/games/blockilization/pictures/l_celts_sheet.png new file mode 100644 index 0000000..c924ce7 Binary files /dev/null and b/src/games/blockilization/pictures/l_celts_sheet.png differ diff --git a/src/games/blockilization/pictures/l_denmark.png b/src/games/blockilization/pictures/l_denmark.png new file mode 100644 index 0000000..0d2197e Binary files /dev/null and b/src/games/blockilization/pictures/l_denmark.png differ diff --git a/src/games/blockilization/pictures/l_denmark_cross.png b/src/games/blockilization/pictures/l_denmark_cross.png new file mode 100644 index 0000000..5f28549 Binary files /dev/null and b/src/games/blockilization/pictures/l_denmark_cross.png differ diff --git a/src/games/blockilization/pictures/l_denmark_glad.png b/src/games/blockilization/pictures/l_denmark_glad.png new file mode 100644 index 0000000..6512c2d Binary files /dev/null and b/src/games/blockilization/pictures/l_denmark_glad.png differ diff --git a/src/games/blockilization/pictures/l_denmark_sheet.png b/src/games/blockilization/pictures/l_denmark_sheet.png new file mode 100644 index 0000000..bf7810e Binary files /dev/null and b/src/games/blockilization/pictures/l_denmark_sheet.png differ diff --git a/src/games/blockilization/pictures/l_ethiopia.png b/src/games/blockilization/pictures/l_ethiopia.png new file mode 100644 index 0000000..81b97b0 Binary files /dev/null and b/src/games/blockilization/pictures/l_ethiopia.png differ diff --git a/src/games/blockilization/pictures/l_ethiopia_cross.png b/src/games/blockilization/pictures/l_ethiopia_cross.png new file mode 100644 index 0000000..c4cdd62 Binary files /dev/null and b/src/games/blockilization/pictures/l_ethiopia_cross.png differ diff --git a/src/games/blockilization/pictures/l_ethiopia_glad.png b/src/games/blockilization/pictures/l_ethiopia_glad.png new file mode 100644 index 0000000..ee141fc Binary files /dev/null and b/src/games/blockilization/pictures/l_ethiopia_glad.png differ diff --git a/src/games/blockilization/pictures/l_ethiopia_sheet.png b/src/games/blockilization/pictures/l_ethiopia_sheet.png new file mode 100644 index 0000000..9adcfcf Binary files /dev/null and b/src/games/blockilization/pictures/l_ethiopia_sheet.png differ diff --git a/src/games/blockilization/pictures/l_netherlands.png b/src/games/blockilization/pictures/l_netherlands.png new file mode 100644 index 0000000..a0756de Binary files /dev/null and b/src/games/blockilization/pictures/l_netherlands.png differ diff --git a/src/games/blockilization/pictures/l_netherlands_cross.png b/src/games/blockilization/pictures/l_netherlands_cross.png new file mode 100644 index 0000000..817b1c9 Binary files /dev/null and b/src/games/blockilization/pictures/l_netherlands_cross.png differ diff --git a/src/games/blockilization/pictures/l_netherlands_glad.png b/src/games/blockilization/pictures/l_netherlands_glad.png new file mode 100644 index 0000000..f4f3ec2 Binary files /dev/null and b/src/games/blockilization/pictures/l_netherlands_glad.png differ diff --git a/src/games/blockilization/pictures/l_netherlands_sheet.png b/src/games/blockilization/pictures/l_netherlands_sheet.png new file mode 100644 index 0000000..1ccdade Binary files /dev/null and b/src/games/blockilization/pictures/l_netherlands_sheet.png differ diff --git a/src/games/blockilization/pictures/l_poland.png b/src/games/blockilization/pictures/l_poland.png new file mode 100644 index 0000000..2a4ec17 Binary files /dev/null and b/src/games/blockilization/pictures/l_poland.png differ diff --git a/src/games/blockilization/pictures/l_poland_cross.png b/src/games/blockilization/pictures/l_poland_cross.png new file mode 100644 index 0000000..23b90c9 Binary files /dev/null and b/src/games/blockilization/pictures/l_poland_cross.png differ diff --git a/src/games/blockilization/pictures/l_poland_glad.png b/src/games/blockilization/pictures/l_poland_glad.png new file mode 100644 index 0000000..572ee44 Binary files /dev/null and b/src/games/blockilization/pictures/l_poland_glad.png differ diff --git a/src/games/blockilization/pictures/l_poland_sheet.png b/src/games/blockilization/pictures/l_poland_sheet.png new file mode 100644 index 0000000..bda2f7d Binary files /dev/null and b/src/games/blockilization/pictures/l_poland_sheet.png differ diff --git a/src/games/blockilization/pictures/l_polynesia.png b/src/games/blockilization/pictures/l_polynesia.png new file mode 100644 index 0000000..e322cce Binary files /dev/null and b/src/games/blockilization/pictures/l_polynesia.png differ diff --git a/src/games/blockilization/pictures/l_polynesia_cross.png b/src/games/blockilization/pictures/l_polynesia_cross.png new file mode 100644 index 0000000..9fb2e70 Binary files /dev/null and b/src/games/blockilization/pictures/l_polynesia_cross.png differ diff --git a/src/games/blockilization/pictures/l_polynesia_glad.png b/src/games/blockilization/pictures/l_polynesia_glad.png new file mode 100644 index 0000000..e49a79c Binary files /dev/null and b/src/games/blockilization/pictures/l_polynesia_glad.png differ diff --git a/src/games/blockilization/pictures/l_polynesia_sheet.png b/src/games/blockilization/pictures/l_polynesia_sheet.png new file mode 100644 index 0000000..8430d01 Binary files /dev/null and b/src/games/blockilization/pictures/l_polynesia_sheet.png differ diff --git a/src/games/blockilization/pictures/l_siam.png b/src/games/blockilization/pictures/l_siam.png new file mode 100644 index 0000000..75f7806 Binary files /dev/null and b/src/games/blockilization/pictures/l_siam.png differ diff --git a/src/games/blockilization/pictures/l_siam_cross.png b/src/games/blockilization/pictures/l_siam_cross.png new file mode 100644 index 0000000..1be1acf Binary files /dev/null and b/src/games/blockilization/pictures/l_siam_cross.png differ diff --git a/src/games/blockilization/pictures/l_siam_glad.png b/src/games/blockilization/pictures/l_siam_glad.png new file mode 100644 index 0000000..90e0941 Binary files /dev/null and b/src/games/blockilization/pictures/l_siam_glad.png differ diff --git a/src/games/blockilization/pictures/l_siam_sheet.png b/src/games/blockilization/pictures/l_siam_sheet.png new file mode 100644 index 0000000..3a97097 Binary files /dev/null and b/src/games/blockilization/pictures/l_siam_sheet.png differ diff --git a/src/games/blockilization/pictures/l_spain.png b/src/games/blockilization/pictures/l_spain.png new file mode 100644 index 0000000..1dcd5e2 Binary files /dev/null and b/src/games/blockilization/pictures/l_spain.png differ diff --git a/src/games/blockilization/pictures/l_spain_cross.png b/src/games/blockilization/pictures/l_spain_cross.png new file mode 100644 index 0000000..b1142b8 Binary files /dev/null and b/src/games/blockilization/pictures/l_spain_cross.png differ diff --git a/src/games/blockilization/pictures/l_spain_glad.png b/src/games/blockilization/pictures/l_spain_glad.png new file mode 100644 index 0000000..949f688 Binary files /dev/null and b/src/games/blockilization/pictures/l_spain_glad.png differ diff --git a/src/games/blockilization/pictures/l_spain_sheet.png b/src/games/blockilization/pictures/l_spain_sheet.png new file mode 100644 index 0000000..1a595b7 Binary files /dev/null and b/src/games/blockilization/pictures/l_spain_sheet.png differ diff --git a/src/games/blockilization/rules/ai/context.ts b/src/games/blockilization/rules/ai/context.ts index 8b4efa5..65ece00 100644 --- a/src/games/blockilization/rules/ai/context.ts +++ b/src/games/blockilization/rules/ai/context.ts @@ -1,11 +1,11 @@ import { sizeOf } from '../armies'; import { allyOf, cityStates } from '../citystates'; -import { atWar, canStand, citiesOf, knows, seesUnit, unitsOf } from '../core'; +import { atWar, canStand, citiesOf, seesUnit, unitsOf } from '../core'; import { dangerCost, dangerZones, type Danger } from '../danger'; import { happiness, type CityYields } from '../economy'; import { apply, eraOf, type Order } from '../game'; import { distance, neighbours, path, within } from '../hex'; -import { ALL, occupants, stepCost } from '../movement'; +import { ALL, canCrossOcean, canEmbark, occupants, stepCost } from '../movement'; import { questsFor, type QuestKind } from '../quests'; import type { City, Game, Player, Unit } from '../state'; import { teammates } from '../teams'; @@ -205,7 +205,7 @@ export function regions(game: Game): Int32Array { export function passable(ctx: Ctx, u: Unit, t: number, passing = false): boolean { const { game } = ctx; const type = UNITS[u.type]; - if (!canStand(game, u.type, t) && !(type.domain === 'land' && TERRAINS[game.tiles[t].terrain].water && knows(game.players[u.owner], 'optics'))) return false; + if (!canStand(game, u.type, t) && !(type.domain === 'land' && TERRAINS[game.tiles[t].terrain].water && canEmbark(game, u.owner))) return false; const city = cityOn(ctx, t); if (city && city.owner !== u.owner) return false; const m = militaryAt(ctx, t); @@ -353,8 +353,8 @@ export function landRoute(game: Game, from: number, to: number, by?: Player, foe * over the ocean with Astronomy), by the lie of the land alone; null if there's none. */ export function seaRoute(game: Game, p: Player, from: number, to: number): number[] | null { - const embark = knows(p, 'optics'); - const ocean = knows(p, 'astronomy'); + const embark = canEmbark(game, p.index); + const ocean = canCrossOcean(game, p.index); return path(game.grid, from, to, (_a, b) => { const tile = game.tiles[b]; if (tile.elevation === 'mountain' || tile.feature === 'ice') return Infinity; diff --git a/src/games/blockilization/rules/ai/military.ts b/src/games/blockilization/rules/ai/military.ts index 1773368..78b7d06 100644 --- a/src/games/blockilization/rules/ai/military.ts +++ b/src/games/blockilization/rules/ai/military.ts @@ -1,9 +1,9 @@ import { sizeOf } from '../armies'; import { canFire, canPillage, cityFireStrength, cityMaxHp, cityStrength, rangeOf, strength } from '../combat'; -import { canStand, citiesOf, knows } from '../core'; +import { canStand, citiesOf } from '../core'; import { weighing } from '../economy'; import { neighbours, within } from '../hex'; -import { ALL, stepCost } from '../movement'; +import { ALL, canCrossOcean, canEmbark, stepCost } from '../movement'; import type { City, Unit } from '../state'; import { teamOf } from '../teams'; import { TERRAINS } from '../terrain'; @@ -607,8 +607,8 @@ function explore(ctx: Ctx, u: Unit) { */ export function exploreAbroad(ctx: Ctx, u: Unit): boolean { const { game, p } = ctx; - if (!knows(p, 'optics')) return false; - const ocean = knows(p, 'astronomy'); + if (!canEmbark(game, p.index)) return false; + const ocean = canCrossOcean(game, p.index); const edge = unknownEdge(ctx); let best = -1; let bestScore = Infinity; diff --git a/src/games/blockilization/rules/ai/personality.ts b/src/games/blockilization/rules/ai/personality.ts index 7b1a6c9..656501e 100644 --- a/src/games/blockilization/rules/ai/personality.ts +++ b/src/games/blockilization/rules/ai/personality.ts @@ -46,6 +46,16 @@ export const PERSONALITIES: Record = { ottomans: L(6, 6, 7, 6, 5, 6, 6, 7, 5, 6), // Suleiman: a lawgiver's cities and a conqueror's fleets zulu: L(5, 2, 10, 4, 3, 3, 4, 3, 2, 10), // Shaka: war, always inca: L(7, 6, 4, 7, 5, 5, 6, 6, 4, 4), // Pachacuti: towns in the hills, roads between them + america: L(9, 5, 5, 6, 7, 6, 4, 7, 5, 6), // George Washington: a continent settled, then made rich + spain: L(7, 6, 6, 5, 4, 5, 9, 7, 4, 7), // Isabella: faith, and the gold of new lands + siam: L(5, 6, 4, 6, 5, 7, 6, 5, 10, 4), // Ramkhamhaeng: a father to the city-states + ethiopia: L(5, 5, 4, 9, 5, 5, 9, 4, 5, 6), // Haile Selassie: faith, and a defence that never gives + celts: L(6, 4, 6, 5, 4, 6, 10, 4, 4, 6), // Boudicca: the old gods of the forest, and a fight for them + carthage: L(7, 5, 5, 5, 5, 4, 4, 9, 5, 6), // Dido: harbours and trade all round the sea + poland: L(6, 5, 7, 6, 5, 8, 5, 5, 5, 6), // Casimir III: policies, and winged horsemen + netherlands: L(5, 5, 3, 6, 6, 6, 4, 10, 6, 4), // William of Orange: merchants and the sea + polynesia: L(9, 6, 5, 4, 4, 7, 5, 5, 4, 6), // Kamehameha: island after island + denmark: L(6, 3, 9, 3, 4, 4, 5, 6, 3, 9), // Harald Bluetooth: raids from the sea }; /** A city-state: keeps its one city and defends it. */ diff --git a/src/games/blockilization/rules/ai/settle.ts b/src/games/blockilization/rules/ai/settle.ts index c1828c2..34d208a 100644 --- a/src/games/blockilization/rules/ai/settle.ts +++ b/src/games/blockilization/rules/ai/settle.ts @@ -1,8 +1,8 @@ -import { knows } from '../core'; import { canFound } from '../game'; import { freshWater, tileYields } from '../economy'; import { neighbours, within } from '../hex'; import type { Unit } from '../state'; +import { canEmbark } from '../movement'; import { teamOf } from '../teams'; import { RESOURCES, TERRAINS } from '../terrain'; import { act, cityOn, civilianAt, coastal, danger, dist, goTo, militaryAt, sameLand, seaRoute, type Ctx } from './context'; @@ -74,7 +74,7 @@ export function bestSite(ctx: Ctx, from: number, settler?: Unit): { tile: number if (tile.owner >= 0 && tile.owner !== ctx.me) continue; // Over the water (with Optics, to embark): a site on the coast of other land, worth a little less for the voyage. const overseas = !sameLand(game, from, t) && !settler?.embarked; - if (overseas && !(knows(p, 'optics') && coastal(game, t))) continue; + if (overseas && !(canEmbark(game, p.index) && coastal(game, t))) continue; if (taken.has(t) || tile.camp || theirs.some((x) => dist(ctx, x, t) < 4)) continue; // Three tiles from its own cities (closer packed, as Civ V's bots do), four from anyone else's. let near = Infinity; diff --git a/src/games/blockilization/rules/ai/strategy.ts b/src/games/blockilization/rules/ai/strategy.ts index c53bd6c..609ba32 100644 --- a/src/games/blockilization/rules/ai/strategy.ts +++ b/src/games/blockilization/rules/ai/strategy.ts @@ -2,11 +2,12 @@ import { sizeOf } from '../armies'; import { allyOf, cityStates } from '../citystates'; import { cityMaxHp, cityStrength } from '../combat'; import { askOf, capitulationProblem, dealer, dealProblem, declinedLately, friends, pacted, warWords, type Deal, type WarWhy } from '../diplomacy'; -import { atWar, capitalOf, citiesOf, knows, note, random } from '../core'; +import { atWar, capitalOf, citiesOf, note, random } from '../core'; import { score } from '../game'; import { within } from '../hex'; import type { City, Game, Player } from '../state'; import { scenarioOf } from '../starts'; +import { canEmbark } from '../movement'; import { masterOf, sameSide, sideOf, teamedWithPeople, teamOf } from '../teams'; import { independence } from '../vassals'; import { TERRAINS } from '../terrain'; @@ -232,7 +233,7 @@ function targetCity(ctx: Ctx, o: Player): { city: number; near: number; amphibio const home = nearestCity(ctx, c.tile); if (!home) continue; const across = !sameLand(game, home.tile, c.tile); - if (across && !(knows(p, 'optics') && coastal(game, c.tile) && seaRoute(game, p, home.tile, c.tile))) continue; + if (across && !(canEmbark(game, p.index) && coastal(game, c.tile) && seaRoute(game, p, home.tile, c.tile))) continue; // (Across the water, a strong or well-guarded city is all the worse: the army lands under it.) const guards = across ? game.units.filter((u) => u.owner === o.index && p.visible[u.tile] && isSoldier(u) && dist(ctx, u.tile, c.tile) <= 2).reduce((s, u) => s + sizeOf(u), 0) : 0; const d = dist(ctx, home.tile, c.tile) * (across ? 0.5 : 1) + (across ? 5 + guards : 0) + cityStrength(game, c) / 5 + (c.capital && mem.aim === 'domination' ? -1 : 0) + (c.capital && racing ? -8 : 0) + (c.founder === ctx.me ? -6 : freeable(ctx, c) ? -3 : 0) + (wanted(c) ? -10 : 0); @@ -337,7 +338,7 @@ export function planWar(ctx: Ctx) { // The city's fallen (to us or anyone): on to their next. if (!city || city.owner !== plan.target) { if (city?.owner === ctx.me) plan.taken = game.turn; - const reach = (c: (typeof ctx.cities)[number]) => ctx.cities.some((m) => sameLand(game, m.tile, c.tile)) || (knows(p, 'optics') && coastal(game, c.tile)); + const reach = (c: (typeof ctx.cities)[number]) => ctx.cities.some((m) => sameLand(game, m.tile, c.tile)) || (canEmbark(game, p.index) && coastal(game, c.tile)); // (Pressing for the capitals: theirs next, if it can be reached.) const race = capitalsRace(ctx); const open = citiesOf(game, plan.target).filter((c) => p.explored[c.tile] && reach(c)); diff --git a/src/games/blockilization/rules/buildings.ts b/src/games/blockilization/rules/buildings.ts index 6226836..95817a0 100644 --- a/src/games/blockilization/rules/buildings.ts +++ b/src/games/blockilization/rules/buildings.ts @@ -126,6 +126,16 @@ export const BUILDING_LIST: Building[] = [ B({ id: 'hammam', name: 'Hammam', cost: 100, upkeep: 1, tech: 'engineering', civ: 'ottomans', replaces: 'aqueduct', effects: { foodKept: 40, happiness: 1 } }), B({ id: 'ikanda', name: 'Ikanda', cost: 75, upkeep: 1, tech: 'bronze_working', civ: 'zulu', replaces: 'barracks', effects: { xp: [{ domain: 'all', xp: 15 }, { domain: 'land', xp: 10 }] }, note: 'A Barracks whose land units start with 10 more experience.' }), B({ id: 'terrace_farm', name: 'Terrace Farm', cost: 65, upkeep: 1, tech: 'pottery', civ: 'inca', replaces: 'granary', effects: { yields: { food: 2 }, tiles: [{ where: { resource: ['wheat', 'bananas', 'deer'] }, yields: { food: 1 } }, { where: { elevation: ['hills'] }, yields: { food: 1 } }] }, note: 'A Granary whose terraces give +1 food on every worked hill.' }), + B({ id: 'film_studio', name: 'Film Studio', cost: 450, upkeep: 3, tech: 'radio', needs: 'museum', civ: 'america', replaces: 'broadcast_tower', effects: { yields: { culture: 3 }, percent: { culture: 33 }, greatWorks: { kind: 'music', slots: 1 }, tourismPercent: 50 }, note: 'A Broadcast Tower whose films go round the world: +50% tourism from the city.' }), + B({ id: 'mission', name: 'Mission', cost: 100, upkeep: 2, tech: 'philosophy', needs: 'shrine', civ: 'spain', replaces: 'temple', effects: { yields: { faith: 2, science: 2 } }, note: 'A Temple that teaches too: +2 science.' }), + B({ id: 'wat', name: 'Wat', cost: 160, upkeep: 3, tech: 'education', needs: 'library', civ: 'siam', replaces: 'university', effects: { yields: { culture: 3 }, percent: { science: 33 }, tiles: [{ where: { feature: ['jungle'] }, yields: { science: 2 } }], specialists: [{ kind: 'scientist', slots: 2 }] }, note: 'A University with +3 culture.' }), + B({ id: 'stele', name: 'Stele', cost: 40, upkeep: 1, civ: 'ethiopia', replaces: 'monument', effects: { yields: { culture: 2, faith: 2 } }, note: 'A Monument with +2 faith.' }), + B({ id: 'ceilidh_hall', name: 'Ceilidh Hall', cost: 220, upkeep: 2, tech: 'acoustics', needs: 'amphitheater', civ: 'celts', replaces: 'opera_house', effects: { yields: { culture: 1 }, happiness: 3, greatWorks: { kind: 'music', slots: 1 } }, note: 'An Opera House with music and dancing for everyone: +3 happiness.' }), + B({ id: 'cothon', name: 'Cothon', cost: 75, upkeep: 1, tech: 'optics', coastal: true, civ: 'carthage', replaces: 'lighthouse', effects: { tiles: [{ where: { terrain: ['coast', 'ocean'] }, yields: { food: 1 } }], xp: [{ domain: 'sea', xp: 15 }], unitProduction: [{ domain: 'sea', percent: 25 }] }, note: 'A Lighthouse with a round inner harbour: ships built a quarter faster.' }), + B({ id: 'ducal_stable', name: 'Ducal Stable', cost: 100, upkeep: 0, tech: 'horseback_riding', nearResource: ['horses', 'cattle', 'sheep'], civ: 'poland', replaces: 'stable', effects: { unitProduction: [{ classes: ['mounted'], percent: 25 }], tiles: [{ where: { improvement: ['pasture'] }, yields: { prod: 1 } }] }, note: 'A Stable with no upkeep that builds mounted units a quarter faster.' }), + B({ id: 'polder', name: 'Polder', cost: 75, upkeep: 2, tech: 'the_wheel', river: true, civ: 'netherlands', replaces: 'water_mill', effects: { yields: { food: 2, prod: 1 }, tiles: [{ where: { feature: ['marsh'] }, yields: { food: 2 } }, { where: { feature: ['floodplains'] }, yields: { food: 1 } }] }, note: 'A Water Mill whose dykes drain the land: +2 food from the marshes the city works, +1 from flood plains. Needs a river or a lake.' }), + B({ id: 'marae', name: 'Marae', cost: 40, upkeep: 1, tech: 'pottery', civ: 'polynesia', replaces: 'shrine', effects: { yields: { faith: 1, culture: 2 } }, note: 'A Shrine where the people gather: +2 culture.' }), + B({ id: 'ring_fortress', name: 'Ring Fortress', cost: 75, upkeep: 1, tech: 'masonry', civ: 'denmark', replaces: 'walls', effects: { defense: 5, hp: 50, xp: [{ domain: 'land', xp: 10 }] }, note: 'Walls round a war camp: land units trained here start with 10 experience.' }), // ----- National wonders NW({ id: 'national_college', name: 'National College', cost: 125, tech: 'philosophy', everyCity: 'library', effects: { yields: { science: 3 }, percent: { science: 50 } } }), diff --git a/src/games/blockilization/rules/citystates.ts b/src/games/blockilization/rules/citystates.ts index db00307..3be2762 100644 --- a/src/games/blockilization/rules/citystates.ts +++ b/src/games/blockilization/rules/citystates.ts @@ -51,6 +51,9 @@ export function standing(game: Game, p: Player, cs: number): Standing { return influenceOf(p, cs) >= FRIEND ? 'friend' : 'neutral'; } +/** How much more food, culture and faith a civ has of its city-state friends and allies: half again for Siam (each gift rounded up). */ +const gifts = (p: Player, n: number) => (civOf(p).ability.effects.special?.includes('city_state_gifts_50') ? Math.ceil(n * 1.5) : n); + /** What being a friend or ally of each city-state is worth this turn: food in every city, happiness. */ export function cityStateBonus(game: Game, p: Player): { food: number; happiness: number } { let food = 0; @@ -59,7 +62,7 @@ export function cityStateBonus(game: Game, p: Player): { food: number; happiness const s = standing(game, p, cs.index); if (s !== 'friend' && s !== 'ally') continue; const k = cs.cityState!.kind; - if (k === 'maritime') food += s === 'ally' ? 2 : 1; + if (k === 'maritime') food += gifts(p, s === 'ally' ? 2 : 1); if (k === 'mercantile') happiness += s === 'ally' ? 3 : 2; } return { food, happiness }; @@ -122,15 +125,15 @@ export function cityStateGives(game: Game, p: Player, cs: Player, as: 'friend' | const gives = (() => { switch (cs.cityState?.kind) { case 'maritime': - return `+${ally ? 2 : 1} food in every city`; + return `+${gifts(p, ally ? 2 : 1)} food in every city`; case 'cultured': - return `+${(ally ? 6 : 3) * era} culture a turn`; + return `+${gifts(p, (ally ? 6 : 3) * era)} culture a turn`; case 'militaristic': return `a unit about every ${Math.round((ally ? 14 : 20) / often)} turns`; case 'mercantile': return `+${ally ? 3 : 2} happiness`; case 'religious': - return `+${ally ? 4 : 2} faith a turn`; + return `+${gifts(p, ally ? 4 : 2)} faith a turn`; default: return ''; } @@ -299,8 +302,8 @@ export function cityStateTurn(game: Game) { if (s !== 'friend' && s !== 'ally') continue; const k = cs.cityState!.kind; const era = 1 + Math.floor(p.techs.length / 10); - if (k === 'cultured') p.culture += (s === 'ally' ? 6 : 3) * era; - if (k === 'religious') p.faith += s === 'ally' ? 4 : 2; + if (k === 'cultured') p.culture += gifts(p, (s === 'ally' ? 6 : 3) * era); + if (k === 'religious') p.faith += gifts(p, s === 'ally' ? 4 : 2); const often = has(effects, 'cs_units_often') ? 2 : 1; if (k === 'militaristic' && game.turn % Math.round((s === 'ally' ? 14 : 20) / often) === cs.index % 7) { const home = capitalOf(game, p.index); diff --git a/src/games/blockilization/rules/civs.ts b/src/games/blockilization/rules/civs.ts index 63897ad..8c8b4ad 100644 --- a/src/games/blockilization/rules/civs.ts +++ b/src/games/blockilization/rules/civs.ts @@ -2,7 +2,7 @@ import type { Effects } from './effects'; import type { TerrainId } from './terrain'; /** - * The twenty civilizations: each with a leader, its colours, its cities' names (in founding order), + * The thirty civilizations: each with a leader, its colours, its cities' names (in founding order), * an ability (the effects of Civilization V's, said our own way), a unique unit and a unique building * (`units.ts`, `buildings.ts`), and where it likes to start. */ @@ -278,6 +278,135 @@ export const CIV_LIST: Civ[] = [ cities: ['Cusco', 'Tiwanaku', 'Quito', 'Ollantaytambo', 'Vilcabamba', 'Vitcos', 'Andahuaylas', 'Huanuco Pampa', 'Cajamarca', 'Arequipa', 'Nasca', 'Ica', 'Pisac', 'Tumbes', 'Chan Chan', 'Pachacamac'], startBias: 'hills', }, + { + id: 'america', + name: 'America', + adjective: 'American', + leader: 'George Washington', + color: '#24348a', + accent: '#f2f2f2', + ability: { name: 'Manifest Destiny', text: '+1 sight for land units; buying tiles costs half the gold.', effects: { sight: [{ domain: 'land', sight: 1 }], tileGoldCost: 50 } }, + unit: 'minuteman', + building: 'film_studio', + cities: ['Washington', 'New York', 'Boston', 'Philadelphia', 'Atlanta', 'Chicago', 'Seattle', 'San Francisco', 'Los Angeles', 'Houston', 'Portland', 'St. Louis', 'Miami', 'Buffalo', 'Detroit', 'New Orleans'], + }, + { + id: 'spain', + name: 'Spain', + adjective: 'Spanish', + leader: 'Isabella', + color: '#7a0a2a', + accent: '#f2c840', + ability: { name: 'Seven Cities of Gold', text: 'Natural wonders give twice the yields and happiness; 100 gold for finding each one (three times as much for the first to find it).', effects: { special: ['natural_wonders_double', 'natural_wonder_gold'] } }, + unit: 'conquistador', + building: 'mission', + cities: ['Madrid', 'Barcelona', 'Seville', 'Cordoba', 'Toledo', 'Santiago', 'Salamanca', 'Murcia', 'Valencia', 'Zaragoza', 'Pamplona', 'Vitoria', 'Santander', 'Oviedo', 'Jaen', 'Granada'], + startBias: 'coast', + }, + { + id: 'siam', + name: 'Siam', + adjective: 'Siamese', + leader: 'Ramkhamhaeng', + color: '#e070a8', + accent: '#f8f0d8', + ability: { name: 'Father Governs Children', text: 'Half again as much food, culture and faith from city-state friends and allies.', effects: { special: ['city_state_gifts_50'] } }, + unit: 'naresuans_elephant', + building: 'wat', + cities: ['Sukhothai', 'Si Satchanalai', 'Muang Saluang', 'Lampang', 'Phitsanulok', 'Kamphaeng Phet', 'Nakhon Chum', 'Vientiane', 'Nakhon Si Thammarat', 'Martaban', 'Nakhon Sawan', 'Chainat', 'Luang Prabang', 'Uthai Thani', 'Chiang Mai', 'Ayutthaya'], + startBias: 'river', + }, + { + id: 'ethiopia', + name: 'Ethiopia', + adjective: 'Ethiopian', + leader: 'Haile Selassie', + color: '#9ac83a', + accent: '#c8282a', + ability: { name: 'Spirit of Adwa', text: '+20% combat strength against civs with more cities than yours.', effects: { combat: [{ percent: 20, when: 'vs_more_cities' }] } }, + unit: 'mehal_sefari', + building: 'stele', + cities: ['Addis Ababa', 'Harar', 'Adwa', 'Lalibela', 'Gondar', 'Mekele', 'Aksum', 'Dire Dawa', 'Bahir Dar', 'Jimma', 'Dessie', 'Debre Markos', 'Nekemte', 'Debre Berhan', 'Asmara', 'Arba Minch'], + startBias: 'hills', + }, + { + id: 'celts', + name: 'Celts', + adjective: 'Celtic', + leader: 'Boudicca', + color: '#3a5a3a', + accent: '#e8d8a8', + ability: { name: 'Druidic Lore', text: '+1 faith from every forest your cities work.', effects: { tiles: [{ where: { feature: ['forest'] }, yields: { faith: 1 } }] } }, + unit: 'pictish_warrior', + building: 'ceilidh_hall', + cities: ['Edinburgh', 'Dublin', 'Cardiff', 'Aberdeen', 'Inverness', 'Cork', 'Glasgow', 'Galway', 'Swansea', 'Armagh', 'Kilkenny', 'Brest', 'Quimper', 'Douglas', 'Tara', 'Dunadd'], + startBias: 'forest', + }, + { + id: 'carthage', + name: 'Carthage', + adjective: 'Carthaginian', + leader: 'Dido', + color: '#8a7aa8', + accent: '#2a1a3a', + ability: { name: 'Phoenician Heritage', text: 'Every coastal city it founds has a free Lighthouse (its Cothon), with no upkeep.', effects: { special: ['free_coastal_lighthouse'] } }, + unit: 'quinquereme', + building: 'cothon', + cities: ['Carthage', 'Utique', 'Hippo Regius', 'Gades', 'Saldae', 'Leptis Magna', 'Hadrumetum', 'Rusadir', 'Lixus', 'Tingis', 'Tipasa', 'Motya', 'Panormus', 'Kerkouane', 'Malaca', 'Tharros'], + startBias: 'coast', + }, + { + id: 'poland', + name: 'Poland', + adjective: 'Polish', + leader: 'Casimir III', + color: '#c8c8d0', + accent: '#c8143c', + ability: { name: 'Solidarity', text: 'A free social policy each time you enter a new era.', effects: { special: ['policy_each_era'] } }, + unit: 'winged_hussar', + building: 'ducal_stable', + cities: ['Krakow', 'Warsaw', 'Gniezno', 'Wroclaw', 'Poznan', 'Gdansk', 'Torun', 'Lublin', 'Lodz', 'Szczecin', 'Bydgoszcz', 'Katowice', 'Plock', 'Sandomierz', 'Lwow', 'Wilno'], + startBias: 'plains', + }, + { + id: 'netherlands', + name: 'Netherlands', + adjective: 'Dutch', + leader: 'William of Orange', + color: '#ff6a10', + accent: '#f2f2f2', + ability: { name: 'Dutch East India Company', text: 'Sea trade routes give +2 gold; a luxury traded away (your last of it) still gives half its happiness.', effects: { special: ['dutch_sea_trade', 'luxuries_traded_half'] } }, + unit: 'sea_beggar', + building: 'polder', + cities: ['Amsterdam', 'Rotterdam', 'Utrecht', 'Groningen', 'Breda', 'Nijmegen', 'Den Haag', 'Haarlem', 'Arnhem', 'Zutphen', 'Maastricht', 'Tilburg', 'Eindhoven', 'Dordrecht', 'Leiden', 'Middelburg'], + startBias: 'coast', + }, + { + id: 'polynesia', + name: 'Polynesia', + adjective: 'Polynesian', + leader: 'Kamehameha', + color: '#2ab0a0', + accent: '#f8f0d8', + ability: { name: 'Wayfinding', text: 'Land units can take to the sea from the start, and cross the ocean; +1 sight at sea.', effects: { special: ['wayfinding'] } }, + unit: 'maori_warrior', + building: 'marae', + cities: ['Honolulu', 'Samoa', 'Tonga', 'Tahiti', 'Rapa Nui', 'Aotearoa', 'Fiji', 'Rarotonga', 'Hiva Oa', 'Nuku Hiva', 'Raiatea', 'Mangareva', 'Tuvalu', 'Tokelau', 'Kiritimati', 'Moorea'], + startBias: 'coast', + }, + { + id: 'denmark', + name: 'Denmark', + adjective: 'Danish', + leader: 'Harald Bluetooth', + color: '#e81e3a', + accent: '#f2f2f2', + ability: { name: 'Viking Fury', text: 'Land units take to the sea and come ashore for a single move, and have +1 move at sea.', effects: { special: ['viking_fury'] } }, + unit: 'berserker', + building: 'ring_fortress', + cities: ['Copenhagen', 'Aarhus', 'Kaupang', 'Ribe', 'Viborg', 'Odense', 'Roskilde', 'Hedeby', 'Uppsala', 'Jelling', 'Trondheim', 'Aalborg', 'Esbjerg', 'Helsingor', 'Lund', 'Birka'], + startBias: 'coast', + }, ]; /** The barbarians: no cities, at war with everyone, coming out of their camps (`barbarians.ts`). Not a civ anyone picks. */ diff --git a/src/games/blockilization/rules/combat.ts b/src/games/blockilization/rules/combat.ts index 316d3f5..d7df154 100644 --- a/src/games/blockilization/rules/combat.ts +++ b/src/games/blockilization/rules/combat.ts @@ -1,6 +1,6 @@ import { formationBonus, formationKind, sizeOf, unitName } from './armies'; import { BUILDINGS } from './buildings'; -import { atWar, canStand, cityAt, cityById, note, random, removeUnit, seesUnit, speed, updateVisibility } from './core'; +import { atWar, canStand, cityAt, cityById, citiesOf, note, random, removeUnit, seesUnit, speed, updateVisibility } from './core'; import { assignCitizens, blockaded, cityEffects, civEffects, garrisoned, has, happiness } from './economy'; import type { CombatWhen } from './effects'; import { cityReligion } from './faith'; @@ -162,8 +162,10 @@ export function strength(game: Game, u: Unit, role: 'attack' | 'defend' | 'range // into a river from outside it, -50% from the sea (Civ V's). if (role === 'attack' && against) { if (vsUnit) add('Flanking', 10 * neighbours(game.grid, vsUnit.tile).filter((n) => n !== u.tile && occupants(game, n).military?.owner === u.owner).length); - if (game.tiles[target].river && !tile.river && !u.embarked) add('Across a river', -20); - if (u.embarked) add('From the sea', -50); + // (Denmark's Berserkers land and ford rivers fighting.) + const amphibious = type.abilities?.includes('amphibious'); + if (game.tiles[target].river && !tile.river && !u.embarked && !amphibious) add('Across a river', -20); + if (u.embarked && !amphibious) add('From the sea', -50); } // Promotions. for (const id of u.promotions) { @@ -182,6 +184,11 @@ export function strength(game: Game, u: Unit, role: 'attack' | 'defend' | 'range if (p.goldenTurns > 0 && has(civ, 'golden_age_moves_combat')) add('Golden age', 10); add(type.domain === 'sea' ? 'Great Admiral' : 'Great General', leadership(game, u, at)); if (type.abilities?.includes('jungle_fighter') && (game.tiles[target].feature === 'forest' || game.tiles[target].feature === 'jungle')) add('Jungle fighter', 33); + // Where it stands: Ethiopia's Mehal Sefari at home, the Celts' Pictish Warriors abroad. + if (type.abilities?.includes('homeland') && tile.owner === u.owner) add('Homeland', 25); + if (type.abilities?.includes('foreign_lands') && tile.owner !== u.owner) add('Foreign lands', 20); + // A Maori war dance next to it unnerves an enemy. + if (game.units.some((o) => UNITS[o.type]?.abilities?.includes('haka') && !o.embarked && atWar(game, u.owner, o.owner) && distance(game.grid, o.tile, at) === 1)) add('Haka', -10); // An unhappy empire fights worse (Civ V: -2% a point below zero, down to -90%). if (!p.cityState && p.civ !== 'barbarian') { const mood = happiness(game, p); @@ -233,6 +240,10 @@ function combatCounts(when: CombatWhen, game: Game, u: Unit, role: 'attack' | 'd return !!vsUnit && game.players[vsUnit.owner]?.civ === 'barbarian'; case 'vs_city_states': return !!game.players[(vsUnit ?? vsCity)?.owner ?? -1]?.cityState; + case 'vs_more_cities': { + const foe = (vsUnit ?? vsCity)?.owner; + return foe !== undefined && citiesOf(game, foe).length > citiesOf(game, u.owner).length; + } case 'beside_friend': return besideFriend(game, u, at); case 'faithful_land': diff --git a/src/games/blockilization/rules/core.ts b/src/games/blockilization/rules/core.ts index 10b0d24..1b1a238 100644 --- a/src/games/blockilization/rules/core.ts +++ b/src/games/blockilization/rules/core.ts @@ -347,6 +347,8 @@ export function unitSight(game: Game, u: Unit, civ = game.players[u.owner] ? civ const type = UNITS[u.type]; let s = (type?.sight ?? 2) + fromPromotions(u, 'sight'); if (type?.abilities?.includes('extra_sight')) s += 1; + // (Polynesia's Wayfinding: further at sea.) + if (u.embarked && game.players[u.owner] && civOf(game.players[u.owner]).ability.effects.special?.includes('wayfinding')) s += 1; if (type) for (const e of civ) for (const x of e.sight ?? []) if (x.domain === type.domain) s += x.sight; for (const w of wondersBeside(game, u.tile)) s += w.sight ?? 0; return s; @@ -357,7 +359,8 @@ export function maxMoves(game: Game, u: Unit): number { const type = UNITS[u.type]; const p = game.players[u.owner]; if (!type || !p) return 0; - if (u.embarked) return knows(p, 'compass') ? 3 : 2; + // (Denmark's Viking Fury: a move more at sea.) + if (u.embarked) return (knows(p, 'compass') ? 3 : 2) + (civOf(p).ability.effects.special?.includes('viking_fury') ? 1 : 0); const civ = civEffects(game, p); let moves = type.moves + fromPromotions(u, 'moves'); for (const e of civ) for (const m of e.moves ?? []) if (m.domain === type.domain && (!m.classes || m.classes.includes(type.class))) moves += m.moves; @@ -430,6 +433,9 @@ function meetSeen(game: Game, p: Player) { /** A name for the middle of a sentence ("the Sky Pillar"). */ export const inSentence = (name: string) => name.replace(/^The /, 'the '); +/** Gold Spain has for finding a natural wonder (before the game's speed; three times as much for the first to find it). */ +export const SEVEN_CITIES_GOLD = 100; + /** * Natural wonders a civilization has come to know of, however it did (its units' sight, a map from * ruins, satellites), found for good: happier for each (`natureHappiness`), and the first civ to find @@ -443,15 +449,20 @@ function findWonders(game: Game, p: Player) { const found = ((game.found ??= {})[w.id] ??= { tile: t, by: [] }); if (found.by.includes(p.index)) continue; found.by.push(p.index); + // (Spain's Seven Cities of Gold: gold for each, three times as much for the first; and twice the happiness.) + const spain = civOf(p).ability.effects.special ?? []; + const cheer = FOUND_HAPPINESS * (spain.includes('natural_wonders_double') ? 2 : 1); + const treasure = spain.includes('natural_wonder_gold') ? Math.floor(SEVEN_CITIES_GOLD * (found.by.length > 1 ? 1 : 3) * speed(game)) : 0; + p.gold += treasure; if (found.by.length > 1) { - note(game, p.index, { text: `You have found ${inSentence(w.name)}, a natural wonder: +${FOUND_HAPPINESS} happiness for good.`, at: t, kind: 'wonder' }); + note(game, p.index, { text: `You have found ${inSentence(w.name)}, a natural wonder: +${cheer} happiness for good${treasure ? ` and ${treasure} gold` : ''}.`, at: t, kind: 'wonder' }); continue; } const golden = Math.floor(FIRST_FINDER_GOLDEN * speed(game)); const gold = Math.floor((w.finderGold ?? 0) * speed(game)); p.goldenPoints += golden; p.gold += gold; - note(game, p.index, { text: `You are the first to find ${inSentence(w.name)}: +${FOUND_HAPPINESS} happiness for good, ${golden} golden age points${gold ? ` and ${gold} gold` : ''}.`, at: t, kind: 'wonder' }); + note(game, p.index, { text: `You are the first to find ${inSentence(w.name)}: +${cheer} happiness for good, ${golden} golden age points${gold + treasure ? ` and ${gold + treasure} gold` : ''}.`, at: t, kind: 'wonder' }); } } diff --git a/src/games/blockilization/rules/earth.ts b/src/games/blockilization/rules/earth.ts index 1cd4b78..5035234 100644 --- a/src/games/blockilization/rules/earth.ts +++ b/src/games/blockilization/rules/earth.ts @@ -206,6 +206,16 @@ export const TRUE_STARTS: Record = { ottomans: [29, 41], zulu: [31.5, -28.5], inca: [-72, -13.5], + america: [-77, 38.9], + spain: [-3.7, 40.4], + siam: [99.8, 17], + ethiopia: [38.7, 9], + celts: [-3.2, 55.9], + carthage: [10.3, 36.9], + poland: [19.9, 50.1], + netherlands: [4.9, 52.4], + polynesia: [174.8, -36.9], + denmark: [12.6, 55.7], cs_venice: [12.3, 45.4], cs_lisbon: [-9.1, 38.7], cs_kyoto: [135.8, 35], diff --git a/src/games/blockilization/rules/economy.ts b/src/games/blockilization/rules/economy.ts index 250052c..2afbbc4 100644 --- a/src/games/blockilization/rules/economy.ts +++ b/src/games/blockilization/rules/economy.ts @@ -8,7 +8,7 @@ import { cityBeliefEffects, cityReligion, religionEffects } from './faith'; import { alliedLuxuries, alliedStrategics, cityStateBonus } from './citystates'; import { banned, fundedGreatPeople, inForce } from './congress'; import { projectEffects } from './projects'; -import { tradeLuxuries, tradeStrategics } from './diplomacy'; +import { lentLuxuries, tradeLuxuries, tradeStrategics } from './diplomacy'; import { aroundYields, naturalWonderOf, natureHappiness } from './nature'; import { SPEEDS, type City, type Game, type Player, type Unit } from './state'; import { teammates } from './teams'; @@ -188,11 +188,15 @@ export function freshWater(game: Game, t: number): boolean { return neighbours(game.grid, t).some((n) => game.tiles[n].terrain === 'lake' || game.tiles[n].feature === 'oasis' || game.tiles[n].river || !!naturalWonderOf(game.tiles[n])?.freshWater); } +/** Whether a civ has twice the yields and happiness of natural wonders (Spain). */ +const seesWondersDouble = (p: Player) => civOf(p).ability.effects.special?.includes('natural_wonders_double') ?? false; + /** What a tile yields to its owner (before a city's buildings): a natural wonder its own yields, nothing else; land by one that enriches it, more. */ export function tileYields(game: Game, t: number, p: Player | null): Yields { const tile = game.tiles[t]; const wonder = naturalWonderOf(tile); - if (wonder) return { ...wonder.yields }; + // (Spain's Seven Cities of Gold: twice what each gives.) + if (wonder) return p && seesWondersDouble(p) ? Object.fromEntries(Object.entries(wonder.yields).map(([k, v]) => [k, (v ?? 0) * 2])) : { ...wonder.yields }; if (tile.elevation === 'mountain' || tile.feature === 'ice') return {}; let y: Yields = tile.elevation === 'hills' ? { ...HILLS.yields } : { ...TERRAINS[tile.terrain].yields }; if (tile.feature) { @@ -594,15 +598,19 @@ function moodOf(game: Game, p: Player, parts?: { label: string; value: number }[ tell('Base', 9); tell('City-state friends', friends); const lux = luxuries(game, p); - const fromLuxuries = lux.size * (4 + civ.reduce((s, e) => s + (e.happinessPerLuxury ?? 0), 0)); + const perLuxury = 4 + civ.reduce((s, e) => s + (e.happinessPerLuxury ?? 0), 0); + const fromLuxuries = lux.size * perLuxury; // Cultural Diplomacy: luxuries from city-state allies half again as cheering. const own = has(civ, 'cs_resources_double') ? ownLuxuries(game, p) : null; const allied = own ? new Set(alliedLuxuries(game, p).filter((r) => lux.has(r) && !own.has(r))).size * 2 : 0; - good += fromLuxuries + allied; + // The Netherlands: a luxury traded away (the last of it) still half as cheering. + const traded = has(civ, 'luxuries_traded_half') ? [...lentLuxuries(game, p).sent.keys()].filter((r) => !lux.has(r) && !banned(game, r)).length * (perLuxury / 2) : 0; + good += fromLuxuries + allied + traded; tell(`Luxuries (${lux.size} kind${lux.size === 1 ? '' : 's'})`, fromLuxuries + allied); + tell('Luxuries traded away', traded); const big = has(civ, 'happiness_per_10_pop') ? cities.reduce((s, c) => s + Math.floor(c.pop / 10), 0) : 0; const faith = religionHappiness(game, p); - const nature = natureHappiness(game, p.index); + const nature = natureHappiness(game, p.index) * (seesWondersDouble(p) ? 2 : 1); good += big + faith + nature; tell('Large cities', big); tell('Religion', faith); diff --git a/src/games/blockilization/rules/effects.ts b/src/games/blockilization/rules/effects.ts index 5f6ac62..ad7387e 100644 --- a/src/games/blockilization/rules/effects.ts +++ b/src/games/blockilization/rules/effects.ts @@ -70,6 +70,8 @@ export type CombatWhen = | 'vs_barbarians' /** Fighting a city-state's units or its city. */ | 'vs_city_states' + /** Fighting a civ with more cities than its own (units or a city). */ + | 'vs_more_cities' /** Another of its owner's military units is next to it. */ | 'beside_friend' /** In its own land around a city that follows its owner's religion, or an enemy's that does. */ diff --git a/src/games/blockilization/rules/game.ts b/src/games/blockilization/rules/game.ts index a996aec..d7a89f0 100644 --- a/src/games/blockilization/rules/game.ts +++ b/src/games/blockilization/rules/game.ts @@ -27,7 +27,7 @@ import { breakFree, release, vassalsTurn } from './vassals'; import { checkEurekas, killDeeds } from './eurekas'; import { clanById, clanHarmed, clanHooks, clanProblem, dealWithClan, type ClanAct } from './clans'; import { merge, mergeProblem, shrink, sizeOf, unitName } from './armies'; -import { canResearch, eraOfCol, ERAS, TECHS } from './techs'; +import { canResearch, eraOfCol, ERA_NAMES, ERAS, TECHS } from './techs'; import { eraStartOf, scenarioOf, startEraOf } from './starts'; import { startSequence, waitsFor, warBegun } from './turns'; import { climateTurn } from './climate'; @@ -364,6 +364,12 @@ export function foundCity(game: Game, owner: number, t: number): City { p.freeBuildings![b] = left - 1; if (!c.buildings.some((x) => isBuilding(x, b))) c.buildings.push(unitFor(p.civ, b, BUILDINGS)); } + // Carthage: a free Lighthouse (its Cothon) in a city by the sea, with no upkeep. + if (civOf(p).ability.effects.special?.includes('free_coastal_lighthouse') && coastal(game, c)) { + const id = unitFor(p.civ, 'lighthouse', BUILDINGS); + c.buildings.push(id); + (c.free ??= []).push(id); + } if (startEraOf(game.settings) > 0 || scenarioOf(game.settings)) foundedInEra(game, p, c, first); assignCitizens(game, c); // A bot's new city starts on a Warrior; a person's asks what to build (Civ V's way: a Warrior in the Middle Ages is no help). @@ -585,6 +591,7 @@ ruinHooks.onRuin = (game, u) => { /** A tech learned: what it starts (Babylon's free scientist, a free tech's choice). */ export function learn(game: Game, p: Player, id: string) { + const era = eraOf(p); if (id === 'future_tech') p.futureTechs++; else if (!p.techs.includes(id)) p.techs.push(id); delete p.research[id]; @@ -595,6 +602,12 @@ export function learn(game: Game, p: Player, id: string) { if (id === 'future_tech' && !p.researching && !Object.keys(TECHS).some((t) => t !== 'future_tech' && canResearch(new Set(p.techs), t))) p.researching = 'future_tech'; note(game, p.index, { text: `You have discovered ${TECHS[id].name}.`, kind: 'tech' }); if (id === 'writing' && has(civEffects(game, p), 'scientist_on_writing')) greatPersonBorn(game, p, 'scientist'); + // Poland's Solidarity: a free social policy for each new era. + const eras = eraOf(p) - era; + if (eras > 0 && civOf(p).ability.effects.special?.includes('policy_each_era')) { + for (let n = 0; n < eras; n++) p.culture += policyCost(game, p, p.policies.length + n); + note(game, p.index, { text: `You have entered the ${ERA_NAMES[ERAS[eraOf(p)]]}: Solidarity gives you ${eras > 1 ? `${eras} free social policies` : 'a free social policy'}.`, kind: 'good' }); + } updateVisibility(game, p); } diff --git a/src/games/blockilization/rules/movement.ts b/src/games/blockilization/rules/movement.ts index 9286b45..c3a9e38 100644 --- a/src/games/blockilization/rules/movement.ts +++ b/src/games/blockilization/rules/movement.ts @@ -55,10 +55,14 @@ export function civiliansAt(game: Game, t: number): Unit[] { export { ALL }; -/** Whether a land unit of this owner may step onto water here (embarking: Optics). */ -const canEmbark = (game: Game, owner: number) => knows(game.players[owner], 'optics'); -/** Whether ocean is open to embarked units and early ships (Astronomy). */ +/** Whether a civ has a special ability (its civilization's own, by name). */ +const civCan = (game: Game, owner: number, special: string) => civOf(game.players[owner]).ability.effects.special?.includes(special) ?? false; +/** Whether a land unit of this owner may step onto water here (embarking: Optics; Polynesia's from the start). */ +export const canEmbark = (game: Game, owner: number) => knows(game.players[owner], 'optics') || civCan(game, owner, 'wayfinding'); +/** Whether ocean is open to early ships (Astronomy). */ const canOcean = (game: Game, owner: number) => knows(game.players[owner], 'astronomy'); +/** Whether ocean is open to this owner's embarked units (Astronomy; Polynesia's from the start). */ +export const canCrossOcean = (game: Game, owner: number) => canOcean(game, owner) || civCan(game, owner, 'wayfinding'); /** Movement points to step from `a` to its neighbour `b` (Infinity: can't). Ignores other units. */ export function stepCost(game: Game, u: Unit, a: number, b: number): number { @@ -76,13 +80,14 @@ export function stepCost(game: Game, u: Unit, a: number, b: number): number { } if (type.domain === 'air') return 1; // Land. + // Embarking or disembarking takes the rest of the turn (a single move for Denmark's); moving at sea, one a tile. + const landing = civCan(game, u.owner, 'viking_fury') ? 1 : ALL; if (water) { if (!canEmbark(game, u.owner)) return Infinity; - if (to.terrain === 'ocean' && !canOcean(game, u.owner)) return Infinity; - // Embarking or disembarking takes the rest of the turn; moving at sea, one a tile. - return TERRAINS[from.terrain].water ? 1 : ALL; + if (to.terrain === 'ocean' && !canCrossOcean(game, u.owner)) return Infinity; + return TERRAINS[from.terrain].water ? 1 : landing; } - if (TERRAINS[from.terrain].water) return ALL; // disembarking ends the move + if (TERRAINS[from.terrain].water) return landing; // disembarking ends the move // The Great Wall: an enemy's land costs every move (until its owner knows Dynamite). if (to.owner >= 0 && to.owner !== u.owner && atWar(game, u.owner, to.owner) && greatWall(game, to.owner)) return ALL; // Roads (both ends) and railroads; for the Iroquois, the forests and jungles of their own land. diff --git a/src/games/blockilization/rules/trade.ts b/src/games/blockilization/rules/trade.ts index 4ae006f..a46eb8f 100644 --- a/src/games/blockilization/rules/trade.ts +++ b/src/games/blockilization/rules/trade.ts @@ -118,6 +118,8 @@ export function routeYield(game: Game, p: Player, r: Route): { gold: number; sci const civ = civEffects(game, p); let gold = 3 + Math.floor(to.pop / 2) + (r.sea ? 2 : 0) + (has(civ, 'trade_gold_2') ? 2 : 0); if (r.sea && has(civ, 'sea_trade_gold_2')) gold += 2; + // (The Netherlands' East India Company: two more, Treasure Fleets' or not.) + if (r.sea && has(civ, 'dutch_sea_trade')) gold += 2; if (them.cityState && has(civ, 'cs_trade_gold_3')) gold += 3; if (p.ideology && them.ideology === p.ideology && has(civ, 'ideology_trade_gold')) gold += 3; const science = Math.max(0, Math.floor((them.techs.length - p.techs.length) / 3)); diff --git a/src/games/blockilization/rules/units.ts b/src/games/blockilization/rules/units.ts index 18a9996..4cf0321 100644 --- a/src/games/blockilization/rules/units.ts +++ b/src/games/blockilization/rules/units.ts @@ -63,6 +63,14 @@ export type Ability = | 'discover_tech' | 'kill_heal' | 'jungle_fighter' + /** Fighting in its own land (Ethiopia's Mehal Sefari). */ + | 'homeland' + /** Fighting outside its own land (the Celts' Pictish Warrior). */ + | 'foreign_lands' + /** Enemies next to it fight worse (Polynesia's Maori Warrior). */ + | 'haka' + /** Attacks from the sea and across rivers as from the land (Denmark's Berserker). */ + | 'amphibious' | 'political_treatise' | 'withdraw'; @@ -208,6 +216,16 @@ export const UNIT_LIST: UnitType[] = [ U({ id: 'janissary', name: 'Janissary', class: 'gun', cost: 150, strength: 26, moves: 2, tech: 'gunpowder', obsolete: 'rifling', upgrade: 'rifleman', civ: 'ottomans', replaces: 'musketman', abilities: ['kill_heal'] }), U({ id: 'impi', name: 'Impi', class: 'melee', cost: 90, strength: 16, moves: 3, tech: 'civil_service', obsolete: 'metallurgy', upgrade: 'lancer', civ: 'zulu', replaces: 'pikeman', abilities: ['bonus_vs_mounted'] }), U({ id: 'slinger', name: 'Slinger', class: 'ranged', cost: 35, strength: 5, ranged: 7, range: 2, moves: 2, tech: 'archery', obsolete: 'machinery', upgrade: 'composite_bowman', civ: 'inca', replaces: 'archer' }), + U({ id: 'minuteman', name: 'Minuteman', class: 'gun', cost: 150, strength: 24, moves: 2, tech: 'gunpowder', obsolete: 'rifling', upgrade: 'rifleman', civ: 'america', replaces: 'musketman', abilities: ['ignore_terrain'] }), + U({ id: 'conquistador', name: 'Conquistador', class: 'mounted', cost: 120, strength: 20, moves: 4, tech: 'chivalry', obsolete: 'military_science', upgrade: 'cavalry', resource: 'horses', civ: 'spain', replaces: 'knight', abilities: ['no_terrain_defense', 'withdraw', 'extra_sight'] }), + U({ id: 'naresuans_elephant', name: "Naresuan's Elephant", class: 'mounted', cost: 120, strength: 25, moves: 3, tech: 'chivalry', obsolete: 'military_science', upgrade: 'cavalry', civ: 'siam', replaces: 'knight', abilities: ['no_terrain_defense'] }), + U({ id: 'mehal_sefari', name: 'Mehal Sefari', class: 'gun', cost: 225, strength: 34, moves: 2, tech: 'rifling', obsolete: 'replaceable_parts', upgrade: 'great_war_infantry', civ: 'ethiopia', replaces: 'rifleman', abilities: ['homeland'] }), + U({ id: 'pictish_warrior', name: 'Pictish Warrior', class: 'melee', cost: 56, strength: 11, moves: 2, tech: 'bronze_working', obsolete: 'civil_service', upgrade: 'pikeman', civ: 'celts', replaces: 'spearman', abilities: ['bonus_vs_mounted', 'foreign_lands'] }), + U({ id: 'quinquereme', name: 'Quinquereme', class: 'naval_melee', domain: 'sea', cost: 56, strength: 13, moves: 4, tech: 'sailing', obsolete: 'astronomy', upgrade: 'caravel', civ: 'carthage', replaces: 'trireme', abilities: ['coast_only'] }), + U({ id: 'winged_hussar', name: 'Winged Hussar', class: 'mounted', cost: 185, strength: 30, moves: 5, tech: 'metallurgy', obsolete: 'combined_arms', upgrade: 'anti_tank_gun', resource: 'horses', civ: 'poland', replaces: 'lancer', abilities: ['no_terrain_defense', 'penalty_vs_cities', 'bonus_vs_mounted'] }), + U({ id: 'sea_beggar', name: 'Sea Beggar', class: 'naval_melee', domain: 'sea', cost: 150, strength: 27, moves: 5, tech: 'navigation', obsolete: 'combustion', upgrade: 'destroyer', civ: 'netherlands', replaces: 'privateer', abilities: ['heal_adjacent'] }), + U({ id: 'maori_warrior', name: 'Maori Warrior', class: 'melee', cost: 40, strength: 8, moves: 2, obsolete: 'metal_casting', upgrade: 'swordsman', civ: 'polynesia', replaces: 'warrior', abilities: ['haka'] }), + U({ id: 'berserker', name: 'Berserker', class: 'melee', cost: 120, strength: 21, moves: 3, tech: 'steel', obsolete: 'gunpowder', upgrade: 'musketman', resource: 'iron', civ: 'denmark', replaces: 'longswordsman', abilities: ['amphibious'] }), ]; export const UNITS: Record = Object.fromEntries(UNIT_LIST.map((u) => [u.id, u])); diff --git a/src/games/blockilization/tests/cases.ts b/src/games/blockilization/tests/cases.ts index 593d01f..96f95be 100644 --- a/src/games/blockilization/tests/cases.ts +++ b/src/games/blockilization/tests/cases.ts @@ -10,7 +10,7 @@ import { PERSONALITIES } from '../rules/ai/personality'; import { UNITS } from '../rules/units'; import { CIV_SYMBOL, glyphPaths } from '../client/icons'; import { atWar, canStand, cityAt, createGame, maxMoves, seesUnit, techCost, unitSight, updateAllVisibility } from '../rules/core'; -import { assignCitizens, cityEffects, cityYields, workedYields, connected, dissent, foodKept, freshWater, goldUpkeep, growthThreshold, greatCost, greatPeopleBonus, happiness, policyCost, purchaseCost, strategic, tileCultureCost, tileYields, UNHAPPINESS_PER_CITY, weighing, workable } from '../rules/economy'; +import { assignCitizens, cityEffects, cityYields, workedYields, connected, dissent, foodKept, freshWater, goldUpkeep, growthThreshold, greatCost, greatPeopleBonus, happiness, policyCost, purchaseCost, strategic, tileCultureCost, tileGoldCost, tileYields, UNHAPPINESS_PER_CITY, weighing, workable } from '../rules/economy'; import { faithCost, faithTurn, faithUnit, foundReligion, maxReligions, prophetCost, religionsLeft, spreadsOf } from '../rules/faith'; import { ANARCHY_TURNS, apply, applyOnce, blockers, canBuild, workTurns, constructible, canChooseIdeology, ideologySwitch, SWITCH_DISSENT, canDig, canFound, checkVictory, endTurn, faithPurchases, healing, learn, plunder, productionToward, score, shareResearch, trainable, trainOptions, upgradeCost, upgradeTo, wellFormed, type Order } from '../rules/game'; import { colOf, distance, line, neighbour, neighbours, rowOf, tileAt, within } from '../rules/hex'; @@ -34,7 +34,7 @@ import { previewAttack, previewCityFire } from '../rules/preview'; import { promotionsDue } from '../rules/promotions'; import { BELIEF_LIST } from '../rules/religion'; import { campCleared, QUEST_REWARD, questsFor, questsOf, questsTurn, type Quest, type QuestKind } from '../rules/quests'; -import { allyOf, cannotBully, cannotCoup, cityStateTurn, coupChance, influenceFall, TRIBUTE_COST, tributeGold, tributeTurns } from '../rules/citystates'; +import { allyOf, cannotBully, cannotCoup, cityStateBonus, cityStateTurn, coupChance, influenceFall, TRIBUTE_COST, tributeGold, tributeTurns } from '../rules/citystates'; import { banned, cannotPropose, delegates, delegateSources, embargoed, inForce, propose as proposeResolution, proposers, sessionTurns, vote as castVote } from '../rules/congress'; import { replayAt, replayEnd, type ReplayEvent } from '../rules/replay'; import { MAP_SIZES, SPEEDS, TURN_LIMITS, type Game, type MapSize, type Settings } from '../rules/state'; @@ -78,7 +78,7 @@ import { botAnswer, EMOTES, isEmote, isPing, moodOf, toneOf } from '../rules/cha import { isMilitary } from '../rules/units'; import { civEffects, has } from '../rules/core'; import { bribeTurns, clanCost, clansOf, clansTurn, clanUnit, hireRest, inciteTurns, newClan, raidTarget, SETTLE, spares } from '../rules/clans'; -import { CLAN_STATES, speed } from '../rules/core'; +import { CLAN_STATES, SEVEN_CITIES_GOLD, speed } from '../rules/core'; import { barbarianTurn } from '../rules/barbarians'; import { clans as botClans } from '../rules/ai/clans'; import { die } from '../rules/combat'; @@ -5420,6 +5420,210 @@ test('the Inca: a hill is a single move; roads on hills cost no upkeep and half eq(roads('inca', true), 0, 'none on hills'); }); +test('America: +1 sight for land units; tiles cost half the gold to buy', () => { + const { game, at } = blank(['america', 'egypt']); + const mine = unit(game, 'warrior', 0, at(4, 4)); + const theirs = unit(game, 'warrior', 1, at(16, 4)); + eq(unitSight(game, mine) - unitSight(game, theirs), 1, 'a tile further'); + const cost = (civ: string) => { + const w = blank([civ, 'egypt']); + const c = city(w.game, 0, w.at(6, 6)); + return tileGoldCost(w.game, c, w.at(6, 9)); + }; + ok(Math.abs(cost('america') / cost('egypt') - 0.5) < 0.02, `half the gold (${cost('america')} against ${cost('egypt')})`); +}); + +test('Spain: natural wonders give twice the yields and happiness; gold for finding each, three times as much for the first', () => { + const { game, at } = blank(['spain', 'egypt']); + const [spain, egypt] = game.players; + city(game, 0, at(3, 8)); + city(game, 1, at(21, 8)); + const peak = at(12, 8); + naturalWonder(game, peak, 'gilded_peak'); + const moods = [happiness(game, spain), happiness(game, egypt)]; + unit(game, 'scout', 1, at(14, 8)); + unit(game, 'scout', 0, at(10, 8)); + eq(game.found?.gilded_peak?.by.join(), '1,0', 'found by Egypt, then Spain'); + eq(happiness(game, spain) - moods[0], 2, 'twice as happy for it'); + eq(happiness(game, egypt) - moods[1], 1, 'Egypt as usual'); + eq(spain.gold, Math.floor(SEVEN_CITIES_GOLD * speed(game)), 'gold for finding it second'); + const mine = tileYields(game, peak, spain); + const theirs = tileYields(game, peak, egypt); + for (const k of Object.keys(theirs) as (keyof typeof theirs)[]) eq(mine[k], 2 * (theirs[k] ?? 0), `twice the ${k}`); + const first = blank(['spain', 'egypt']); + naturalWonder(first.game, first.at(12, 8), 'gilded_peak'); + unit(first.game, 'scout', 0, first.at(10, 8)); + eq(first.game.players[0].gold, Math.floor(SEVEN_CITIES_GOLD * 3 * speed(first.game)) + Math.floor(100 * speed(first.game)), 'three times as much first (and the Gilded Peak’s own gold)'); +}); + +test('Siam: half again as much food, culture and faith from city-state friends', () => { + const gifts = (civ: string) => { + const { game, at } = blank([civ, 'egypt'], { cityStates: 3 }); + const css = game.players.filter((x) => x.cityState); + (['maritime', 'cultured', 'religious'] as const).forEach((k, i) => { + css[i].cityState!.kind = k; + city(game, css[i].index, at(6 + i * 6, 12)); + }); + const p = game.players[0]; + city(game, 0, at(3, 3)); + for (const cs of css) p.influence[cs.index] = 40; + const food = cityStateBonus(game, p).food; + [p.culture, p.faith] = [0, 0]; + cityStateTurn(game); + return { food, culture: p.culture, faith: p.faith }; + }; + const siam = gifts('siam'); + const egypt = gifts('egypt'); + eq(egypt.food, 1, 'a maritime friend: +1 food'); + eq(siam.food, 2, 'half again, rounded up, for Siam'); + eq(siam.culture, Math.ceil(egypt.culture * 1.5), 'culture'); + eq(siam.faith, Math.ceil(egypt.faith * 1.5), 'faith'); + ok(egypt.culture > 0 && egypt.faith > 0, 'from cultured and religious friends'); +}); + +test('Ethiopia: +20% against civs with more cities than its own', () => { + const { game, at } = blank(['ethiopia', 'egypt']); + war(game, 0, 1); + city(game, 0, at(3, 3)); + city(game, 1, at(20, 3)); + const mine = unit(game, 'warrior', 0, at(10, 9)); + const foe = unit(game, 'warrior', 1, at(11, 9)); + near(strength(game, mine, 'attack', foe, mine.tile), 8, 'as many cities: none'); + city(game, 1, at(20, 14)); + near(strength(game, mine, 'attack', foe, mine.tile), 8 * 1.2, 'they have more'); + near(strength(game, foe, 'attack', mine, foe.tile), 8, 'not for them'); +}); + +test('the Celts: +1 faith from each forest their cities work', () => { + const faith = (civ: string) => { + const { game, at } = blank([civ, 'egypt']); + const c = city(game, 0, at(6, 6)); + const t = neighbours(game.grid, c.tile)[0]; + game.tiles[t].feature = 'forest'; + return workedYields(game, c, t).faith ?? 0; + }; + eq(faith('celts') - faith('egypt'), 1, 'a faith more'); +}); + +test('Carthage: a free Lighthouse (its Cothon), with no upkeep, in every coastal city it founds', () => { + const found = (civ: string, coast: boolean) => { + const { game, at } = blank([civ, 'egypt']); + if (coast) game.tiles[neighbours(game.grid, at(6, 6))[0]].terrain = 'coast'; + const c = city(game, 0, at(6, 6)); + return { buildings: c.buildings, upkeep: goldUpkeep(game, game.players[0]).buildings }; + }; + const carthage = found('carthage', true); + ok(carthage.buildings.includes('cothon'), 'a Cothon by the sea'); + eq(carthage.upkeep, 0, 'that costs nothing'); + ok(!found('carthage', false).buildings.includes('cothon'), 'none inland'); + ok(!found('egypt', true).buildings.some((b) => b === 'lighthouse'), 'none for others'); +}); + +test('Poland: a free social policy on entering each new era', () => { + const classical = TECH_LIST.find((t) => t.col > 2 && t.col <= 4)!.id; + const run = (civ: string) => { + const { game } = blank([civ, 'egypt']); + const p = game.players[0]; + p.culture = 0; + learn(game, p, 'pottery'); + const ancient = p.culture; + const cost = policyCost(game, p); + learn(game, p, classical); + return { ancient, classical: p.culture, cost }; + }; + const poland = run('poland'); + eq(poland.ancient, 0, 'nothing within the Ancient Era'); + eq(poland.classical, poland.cost, 'a policy’s culture on reaching the Classical'); + eq(run('egypt').classical, 0, 'nothing for others'); +}); + +test('the Netherlands: +2 gold from sea trade routes; a luxury traded away keeps half its happiness', () => { + const { game, at } = blank(['netherlands', 'egypt']); + const mine = city(game, 0, at(3, 3), { pop: 4 }); + const theirs = city(game, 1, at(12, 3), { pop: 4 }); + const gold = (p: number, sea: boolean) => routeYield(game, game.players[p], { from: p ? theirs.id : mine.id, to: p ? mine.id : theirs.id, turns: 10, sea }).gold; + eq(gold(0, true) - gold(0, false) - (gold(1, true) - gold(1, false)), 2, 'two more by sea'); + const traded = (civ: string) => { + const w = blank([civ, 'babylon']); + const c = city(w.game, 0, w.at(3, 3)); + city(w.game, 1, w.at(20, 3)); + Object.assign(w.game.tiles[neighbours(w.game.grid, c.tile)[0]], { resource: 'silver', improvement: 'mine' }); + const p = w.game.players[0]; + const before = happiness(w.game, p); + makeDeal(w.game, { from: 0, to: 1, items: [{ kind: 'luxury', from: 0, resource: 'silver' }] }); + return before - happiness(w.game, p); + }; + eq(traded('egypt'), 4, 'others lose all of it'); + eq(traded('netherlands'), 2, 'the Dutch half'); + const polder = city(game, 0, at(6, 12)); + game.tiles[polder.tile].river = true; + const [marsh, flood] = neighbours(game.grid, polder.tile); + game.tiles[marsh].feature = 'marsh'; + Object.assign(game.tiles[flood], { terrain: 'desert', feature: 'floodplains' }); + const before = [workedYields(game, polder, marsh).food ?? 0, workedYields(game, polder, flood).food ?? 0]; + polder.buildings.push('polder'); + eq((workedYields(game, polder, marsh).food ?? 0) - before[0], 2, 'the Polder: +2 food from a marsh'); + eq((workedYields(game, polder, flood).food ?? 0) - before[1], 1, '+1 from flood plains'); +}); + +test('Polynesia: land units take to the sea and cross the ocean from the start, and see further there', () => { + const step = (civ: string, terrain: 'coast' | 'ocean') => { + const { game, at } = blank([civ, 'egypt']); + game.tiles[at(6, 6)].terrain = terrain; + const u = unit(game, 'warrior', 0, at(5, 6)); + return stepCost(game, u, at(5, 6), at(6, 6)); + }; + ok(Number.isFinite(step('polynesia', 'coast')) && Number.isFinite(step('polynesia', 'ocean')), 'onto the coast and the ocean'); + eq(step('egypt', 'coast'), Infinity, 'not others before Optics'); + const { game, at } = blank(['polynesia', 'egypt']); + for (const t of [at(6, 6), at(16, 6)]) game.tiles[t].terrain = 'ocean'; + const mine = unit(game, 'warrior', 0, at(6, 6), { embarked: true }); + const theirs = unit(game, 'warrior', 1, at(16, 6), { embarked: true }); + eq(unitSight(game, mine) - unitSight(game, theirs), 1, 'a tile further at sea'); +}); + +test('Denmark: taking to the sea and landing take a single move; a move more at sea', () => { + const steps = (civ: string) => { + const { game, at } = blank([civ, 'egypt']); + game.players[0].techs.push('optics'); + game.tiles[at(6, 6)].terrain = 'coast'; + const u = unit(game, 'warrior', 0, at(5, 6)); + const out = [stepCost(game, u, at(5, 6), at(6, 6)), stepCost(game, u, at(6, 6), at(5, 6))]; + u.embarked = true; + return { out, moves: maxMoves(game, u) }; + }; + eq(steps('denmark').out.join(), '1,1', 'a move each way'); + ok(steps('egypt').out.every((c) => c >= 99), 'every move for others'); + eq(steps('denmark').moves - steps('egypt').moves, 1, 'a move more at sea'); +}); + +test('the new unique units: the Mehal Sefari at home, Pictish Warriors abroad, the Maori war dance, Berserkers from the sea', () => { + const { game, at } = blank(['ethiopia', 'celts']); + war(game, 0, 1); + const home = city(game, 0, at(3, 8)); + city(game, 1, at(21, 8)); + const inLand = neighbours(game.grid, home.tile)[0]; + const sefari = unit(game, 'mehal_sefari', 0, inLand); + const foe = unit(game, 'rifleman', 1, neighbours(game.grid, inLand).find((t) => t !== home.tile && game.tiles[t].owner !== 1 && !game.units.some((u) => u.tile === t))!); + near(strength(game, sefari, 'attack', foe, sefari.tile), 34 * 1.25, 'Mehal Sefari: +25% in its own land'); + const away = unit(game, 'mehal_sefari', 0, at(12, 14)); + near(strength(game, away, 'attack', unit(game, 'rifleman', 1, at(13, 14)), away.tile), 34, 'none away'); + const pict = unit(game, 'pictish_warrior', 1, at(12, 2)); + near(strength(game, pict, 'attack', unit(game, 'warrior', 0, at(13, 2)), pict.tile), 11 * 1.2, 'Pictish Warrior: +20% outside its land'); + const w = blank(['polynesia', 'denmark']); + war(w.game, 0, 1); + const maori = unit(w.game, 'maori_warrior', 0, w.at(10, 8)); + const dane = unit(w.game, 'longswordsman', 1, w.at(11, 8)); + const far = unit(w.game, 'longswordsman', 1, w.at(16, 8)); + near(strength(w.game, dane, 'attack', maori, dane.tile), 21 * 0.9, 'beside a Maori Warrior: -10%'); + near(strength(w.game, far, 'attack', unit(w.game, 'warrior', 0, w.at(17, 8)), far.tile), 21, 'none further off'); + near(strength(w.game, maori, 'attack', dane, maori.tile), 8, 'not for the Maori'); + const berserker = unit(w.game, 'berserker', 1, w.at(10, 14), { embarked: true }); + const sword = unit(w.game, 'longswordsman', 1, w.at(14, 14), { embarked: true }); + near(strength(w.game, berserker, 'attack', unit(w.game, 'warrior', 0, w.at(11, 14)), berserker.tile), 21, 'Berserker: no penalty from the sea'); + near(strength(w.game, sword, 'attack', unit(w.game, 'warrior', 0, w.at(15, 14)), sword.tile), 21 * 0.5, 'half for others'); +}); + // ------------------------------------------------------------------------------------------------- // A living world: seasons, disasters, the rising sea // ------------------------------------------------------------------------------------------------- diff --git a/src/games/blockilization/tests/sweep.ts b/src/games/blockilization/tests/sweep.ts index c3c5686..9507800 100644 --- a/src/games/blockilization/tests/sweep.ts +++ b/src/games/blockilization/tests/sweep.ts @@ -111,7 +111,7 @@ function sweepOf(civs: string[]) { claim(game, a, t); }); }); - for (const [i, other] of cs.entries()) city(game, other.index, at(4 + i * 12, 24), { pop: 5 }); + for (const [i, other] of cs.entries()) city(game, other.index, at(4 + i * 8, 24), { pop: 5 }); // Everyone's religion holds every belief (the founder's and its followers' beliefs, all at work). majors.forEach((p, i) => { const id = `r${i}`; diff --git a/src/games/blockilization/tools/portraits.mjs b/src/games/blockilization/tools/portraits.mjs index f53c549..3766e4f 100644 --- a/src/games/blockilization/tools/portraits.mjs +++ b/src/games/blockilization/tools/portraits.mjs @@ -362,6 +362,150 @@ const BUSTS = [ for (let y = 0; y < 6; y += 2) for (let x = -8; x < 8; x += 2) m.box([x, y, 4], [x + 1, y + 1, 5], (x + y) % 4 ? 'gold' : 'redDark'); }, }), + bust('l_america', { + robe: 'blueDark', + skin: 'skin3', + hair: 'hairWhite', + extra: (m) => { + // Powdered hair rolled at the sides and tied back with a black ribbon; a blue coat with buff facings, a white stock and gold epaulettes. + m.box([-7, 12, -3], [-5, 14, 1], 'hairWhite').box([5, 12, -3], [7, 14, 1], 'hairWhite'); + m.box([-1, 11, -7], [1, 17, -5], 'hairWhite').box([-2, 15, -8], [2, 17, -7], 'black'); + m.box([-5, 0, 4], [-2, 7, 5], 'sand').box([2, 0, 4], [5, 7, 5], 'sand'); + m.box([-2, 5, 4], [2, 9, 5], 'white'); + m.box([-11, 6, -3], [-8, 8, 3], 'gold').box([8, 6, -3], [11, 8, 3], 'gold'); + for (let y = 1; y < 5; y += 2) m.set(-2, y, 5, 'gold').set(1, y, 5, 'gold'); + }, + }), + bust('l_spain', { + robe: 'crimson', + skin: 'skin3', + hair: 'orange', + extra: (m) => { + // A white veil framing the face and falling to the shoulders, a gold crown on it; a gold cross on the gown. + m.box([-6, 16, -6], [6, 21, 4], 'white').box([-6, 19, 4], [6, 21, 5], 'white'); + m.box([-6, 8, -6], [-5, 16, 4], 'white').box([5, 8, -6], [6, 16, 4], 'white').box([-6, 7, -7], [6, 19, -6], 'white'); + m.box([-5, 21, -5], [5, 23, 5], 'gold'); + for (const x of [-5, -1, 3]) m.box([x, 23, 3], [x + 2, 25, 5], 'gold'); + m.box([-1, 21, 5], [1, 22, 6], 'redDark'); + m.box([-9, 6, -4], [9, 7, 5], 'gold'); + m.box([-1, 0, 4], [1, 5, 5], 'gold').box([-2, 3, 4], [2, 4, 5], 'gold'); + }, + }), + bust('l_siam', { + robe: 'white', + skin: 'skin2', + nose: 'skin4', + hair: 'black', + extra: (m) => { + // The chada: a gold crown rising in tiers to a spire, wings over the ears; a broad jewelled gold collar. + m.box([-6, 17, -6], [6, 19, 6], 'gold'); + m.box([-5, 19, -5], [5, 21, 5], 'gold').box([-4, 21, -4], [4, 23, 4], 'sash').box([-3, 23, -3], [3, 25, 3], 'gold'); + m.box([-2, 25, -2], [2, 27, 2], 'sash').box([-1, 27, -1], [1, 32, 1], 'gold'); + for (const x of [-4, -1, 2]) m.set(x, 18, 6, 'jade'); + m.box([-7, 13, -2], [-6, 18, 1], 'gold').box([6, 13, -2], [7, 18, 1], 'gold'); + m.box([-9, 5, -4], [9, 8, 5], 'gold'); + for (const x of [-7, -4, -1, 2, 5]) m.set(x, 6, 5, x % 2 ? 'redDark' : 'jade'); + }, + }), + bust('l_ethiopia', { + robe: 'khaki', + skin: 'skin4', + nose: 'horseDark', + hair: 'black', + beard: 'black', + extra: (m) => { + // A peaked officer's cap with a gold badge; a dress uniform with a sash and rows of medals. + m.box([-6, 18, -6], [6, 21, 5], 'khaki').box([-6, 17, -6], [6, 18, 5], 'black'); + m.box([-5, 17, 5], [5, 18, 7], 'black').box([-1, 19, 5], [1, 21, 6], 'gold'); + for (let y = 0; y < 6; y++) m.box([4 - y * 2, y, 4], [6 - y * 2, y + 1, 5], 'green'); + for (const [x, c] of [[2, 'gold'], [3, 'red'], [4, 'green'], [5, 'gold']]) m.set(x, 5, 5, c).set(x, 4, 5, c); + m.box([-9, 6, -4], [-7, 8, 3], 'gold').box([7, 6, -4], [9, 8, 3], 'gold'); + }, + }), + bust('l_celts', { + robe: 'green', + skin: 'skin3', + hair: 'orange', + extra: (m) => { + // Long red hair falling past the shoulders; blue woad on the cheeks; a gold torc; a cloak in a check. + m.box([-7, 3, -6], [-5, 18, 2], 'orange').box([5, 3, -6], [7, 18, 2], 'orange').box([-6, 2, -7], [6, 19, -5], 'orange'); + for (const [x, y] of [[-4, 10], [-4, 11], [3, 10], [3, 11]]) m.set(x, y, 4, 'blue'); + m.box([-4, 8, 3], [4, 9, 6], 'gold').box([-4, 8, 5], [-3, 10, 6], 'gold').box([3, 8, 5], [4, 10, 6], 'gold'); + for (let y = 0; y < 7; y++) for (let x = -9; x < 9; x++) if ((x + 9) % 4 === 0 || y % 4 === 0) m.set(x, y, 4, (x + y) % 2 ? 'redDark' : 'cypress'); + }, + }), + bust('l_carthage', { + robe: 'purple', + skin: 'skin2', + nose: 'skin4', + hair: 'black', + extra: (m) => { + // Curled black hair falling down her back under a gold diadem; gold earrings; a crescent of gold on a purple robe edged in gold. + m.box([-7, 6, -6], [-5, 17, 1], 'black').box([5, 6, -6], [7, 17, 1], 'black').box([-6, 5, -7], [6, 19, -5], 'black'); + for (const [x, y] of [[-7, 8], [-7, 11], [-7, 14], [6, 9], [6, 12], [6, 15]]) m.set(x, y, 1, 'black'); + m.box([-6, 17, -6], [6, 18, 5], 'gold').box([-1, 17, 5], [1, 19, 6], 'gold'); + m.box([-7, 9, 0], [-6, 11, 1], 'gold').box([6, 9, 0], [7, 11, 1], 'gold'); + m.box([-9, 6, -4], [9, 7, 5], 'gold'); + m.box([-2, 3, 4], [2, 4, 5], 'gold').set(-2, 4, 4, 'gold').set(1, 4, 4, 'gold'); + }, + }), + bust('l_poland', { + robe: 'red', + skin: 'skin3', + hair: 'hair', + beard: 'hair', + extra: (m) => { + // Long hair and a full beard under a closed gold crown topped with a cross; an ermine collar on a red robe. + m.box([-7, 8, -6], [-5, 16, 2], 'hair').box([5, 8, -6], [7, 16, 2], 'hair').box([-6, 7, -7], [6, 18, -5], 'hair'); + m.box([-5, 19, -5], [5, 21, 5], 'gold'); + for (const [x, z] of [[-5, 3], [-1, 3], [3, 3], [-5, -5], [3, -5]]) m.box([x, 21, z], [x + 2, 23, z + 2], 'gold'); + m.box([-1, 21, -4], [1, 25, 4], 'gold').box([-1, 25, -1], [1, 28, 1], 'gold').box([-2, 26, -1], [2, 27, 1], 'gold'); + m.set(-4, 20, 5, 'red').set(0, 20, 5, 'jade').set(3, 20, 5, 'red'); + m.box([-9, 5, -4], [9, 8, 5], 'ermine'); + for (const x of [-7, -3, 2, 6]) m.set(x, 6, 4, 'black'); + }, + }), + bust('l_netherlands', { + robe: 'black', + skin: 'skin3', + hair: 'hair', + beard: 'hair', + extra: (m) => { + // A flat black cap; a white ruff; an orange sash across a black doublet. + m.box([-6, 18, -6], [6, 20, 6], 'black').box([-5, 20, -5], [5, 21, 5], 'black'); + m.box([-7, 6, -4], [7, 8, 5], 'white').box([-6, 8, -3], [6, 9, 4], 'white'); + for (let y = 0; y < 6; y++) m.box([-6 + y * 2, y, 4], [-3 + y * 2, y + 1, 5], 'orange'); + }, + }), + bust('l_polynesia', { + robe: 'gold', + skin: 'skin4', + nose: 'horseDark', + hair: 'black', + extra: (m) => { + // The feathered helmet with its crest; a cloak of yellow and red feathers. + m.box([-6, 17, -6], [6, 20, 5], 'gold').box([-5, 20, -5], [5, 21, 4], 'gold'); + m.box([-1, 20, -6], [1, 25, 4], 'red').box([-1, 18, 5], [1, 20, 6], 'red'); + m.box([-6, 17, 4], [6, 18, 5], 'red'); + for (let y = 0; y < 8; y++) for (let x = -9; x < 9; x++) if (Math.abs(x + 0.5) < y - 1 && y < 7) m.set(x, y, 4, 'red'); + m.box([-9, 7, -4], [9, 8, 5], 'red'); + }, + }), + bust('l_denmark', { + robe: 'leather', + skin: 'skin3', + hair: 'straw', + beard: 'straw', + extra: (m) => { + // An iron helmet with a ridge and a nose guard; a great fair beard, braided; a fur collar fastened with a blue stone. + m.box([-6, 17, -6], [6, 21, 5], 'iron').box([-5, 21, -5], [5, 22, 4], 'iron').box([-1, 21, -6], [1, 23, 5], 'steel'); + m.box([-6, 16, 4], [6, 17, 5], 'steel').box([-1, 14, 5], [1, 17, 6], 'steel'); + m.box([-7, 8, -5], [-5, 16, 1], 'straw').box([5, 8, -5], [7, 16, 1], 'straw'); + m.box([-2, 4, 5], [-1, 7, 6], 'straw').box([1, 4, 5], [2, 7, 6], 'straw'); + m.box([-9, 5, -4], [9, 8, 5], 'hairGrey'); + m.box([-1, 5, 5], [1, 7, 6], 'nemes'); + }, + }), ]; /** The leaders' pictures, each in three moods. */