diff --git a/public/games/age-of-dwarves/data/buildings/mundane_wonders.json b/public/games/age-of-dwarves/data/buildings/mundane_wonders.json new file mode 100644 index 00000000..222256a4 --- /dev/null +++ b/public/games/age-of-dwarves/data/buildings/mundane_wonders.json @@ -0,0 +1,260 @@ +[ + { + "id": "mead_hall", + "name": "The Great Mead Hall", + "description": "The first true gathering-hall of the clans — a timbered long-house where saga, drink, and oath are shared across every hearth in the empire.", + "placement": "city", + "category": "culture", + "school": null, + "cost": 150, + "upkeep": 0, + "tech_required": null, + "culture_required": "oral_tradition", + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 1, + "effects": [ + { "type": "happiness", "value": 1 }, + { "type": "culture", "value": 2 } + ], + "sprite": "sprites/buildings/wonders/mead_hall.png", + "flavor": "We sing the dead back into the room. They answer in the chorus.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "culture", "tier1"] } + }, + { + "id": "ancestral_forge", + "name": "The Ancestral Forge", + "description": "The oldest surviving foundry, raised when the clans first learned to smelt copper. Its anvil is said to remember every hammer that struck it.", + "placement": "city", + "category": "production", + "school": null, + "cost": 180, + "upkeep": 0, + "tech_required": "smelting", + "culture_required": null, + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 2, + "effects": [ + { "type": "production", "value": 2 }, + { "type": "production_percent_capital", "value": 0.15 } + ], + "sprite": "sprites/buildings/wonders/ancestral_forge.png", + "flavor": "The first fire was the first empire. The rest was just keeping it lit.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "production", "tier2"] } + }, + { + "id": "hall_of_ancestors", + "name": "Hall of Ancestors", + "description": "A vaulted gallery where the likeness of every clan-father is cut into pillar and wall. The accumulated weight of remembrance steadies every hearth in the empire.", + "placement": "city", + "category": "culture", + "school": null, + "cost": 260, + "upkeep": 0, + "tech_required": null, + "culture_required": "ancestor_shrines", + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 3, + "effects": [ + { "type": "culture", "value": 3 }, + { "type": "border_growth_percent", "value": 0.25 }, + { "type": "happiness", "value": 1 } + ], + "sprite": "sprites/buildings/wonders/hall_of_ancestors.png", + "flavor": "They watch. They always watch. It is enough.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "culture", "tier3"] } + }, + { + "id": "the_deep_road", + "name": "The Deep Road", + "description": "A lightless highway cut mountain to mountain beneath the surface world, wide enough for wagon-trains and defended at every gate. Every city becomes a neighbour of every other.", + "placement": "city", + "category": "infrastructure", + "school": null, + "cost": 320, + "upkeep": 0, + "tech_required": "civil_engineering", + "culture_required": null, + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 4, + "effects": [ + { "type": "connect_all_cities", "value": 1 }, + { "type": "trade_per_city", "value": 1 }, + { "type": "gold", "value": 2 } + ], + "sprite": "sprites/buildings/wonders/the_deep_road.png", + "flavor": "No weather. No bandits. No sun. Only the road, and it keeps.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "infrastructure", "tier4"] } + }, + { + "id": "royal_runestone", + "name": "Royal Runestone", + "description": "A pillar of runecut granite raised at the heart of the capital, bearing the compact between crown and clan. Every new city inherits a fragment of its authority.", + "placement": "city", + "category": "culture", + "school": null, + "cost": 340, + "upkeep": 0, + "tech_required": null, + "culture_required": "hall_of_memory", + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 5, + "effects": [ + { "type": "happiness_per_city", "value": 1 }, + { "type": "culture", "value": 3 }, + { "type": "unrest_reduction", "value": 1 } + ], + "sprite": "sprites/buildings/wonders/royal_runestone.png", + "flavor": "The oath is in the stone. Break it, and the stone breaks you.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "culture", "tier5"] } + }, + { + "id": "the_great_forge", + "name": "The Great Forge", + "description": "The largest foundry ever cut into living stone, its anvils never cool. Every hammer across the empire strikes a little truer when the Great Forge is alight.", + "placement": "city", + "category": "production", + "school": null, + "cost": 420, + "upkeep": 0, + "tech_required": "high_smithing", + "culture_required": null, + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 6, + "effects": [ + { "type": "production", "value": 4 }, + { "type": "production_percent", "value": 0.5 } + ], + "sprite": "sprites/buildings/wonders/the_great_forge.png", + "flavor": "The fires have not guttered in a hundred generations. Neither have we.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "production", "tier6"] } + }, + { + "id": "iron_crown", + "name": "The Iron Crown", + "description": "A war-diadem forged from the steel of every defeated foe. To wear it is to inherit their discipline; to lose it is to gift that discipline to the victor.", + "placement": "city", + "category": "culture", + "school": null, + "cost": 460, + "upkeep": 0, + "tech_required": null, + "culture_required": "world_heritage", + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 7, + "effects": [ + { "type": "unit_xp_start", "value": 10 }, + { "type": "golden_age_on_capture", "value": 1 }, + { "type": "legacy_score", "value": 5 } + ], + "sprite": "sprites/buildings/wonders/iron_crown.png", + "flavor": "Heavy is the crown. Heavier the head that takes it from you.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "culture", "tier7"] } + }, + { + "id": "undermount_vault", + "name": "Undermount Vault", + "description": "A sealed treasury carved beneath the root of the mountain, safer than any bank and older than any coin in it. Its very existence steadies markets across the realm.", + "placement": "city", + "category": "economy", + "school": null, + "cost": 500, + "upkeep": 0, + "tech_required": "steam_metallurgy", + "culture_required": null, + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 8, + "effects": [ + { "type": "gold", "value": 3 }, + { "type": "gold_percent", "value": 0.25 }, + { "type": "science_percent", "value": 0.1 } + ], + "sprite": "sprites/buildings/wonders/undermount_vault.png", + "flavor": "A coin laid in the Vault is a coin outliving its king.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "economy", "tier8"] } + }, + { + "id": "ancestral_archive", + "name": "Ancestral Archive", + "description": "A tiered library of carved stone codices indexing every saga, treaty, and engineering plan the dwarves have ever written down. Its catalogue alone grants a lifetime of learning.", + "placement": "city", + "category": "research", + "school": null, + "cost": 520, + "upkeep": 0, + "tech_required": "high_lore", + "culture_required": null, + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 8, + "effects": [ + { "type": "science", "value": 3 }, + { "type": "science_percent", "value": 0.25 }, + { "type": "free_tech", "value": 1 } + ], + "sprite": "sprites/buildings/wonders/ancestral_archive.png", + "flavor": "What we remember, we do not have to learn again.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "research", "tier8"] } + }, + { + "id": "monument_of_ages", + "name": "Monument of Ages", + "description": "A single carved obelisk the height of a mountain spur, inscribed with the complete chronicle of the dwarven people. Its mere completion is reckoned a victory of memory over time.", + "placement": "city", + "category": "culture", + "school": null, + "cost": 600, + "upkeep": 0, + "tech_required": null, + "culture_required": "apotheosis_of_culture", + "race_required": null, + "wonder_type": "world", + "unique": true, + "mana_generated": null, + "tier": 9, + "effects": [ + { "type": "culture", "value": 8 }, + { "type": "culture_percent", "value": 0.3 }, + { "type": "legacy_score", "value": 10 }, + { "type": "tourism", "value": 5 } + ], + "sprite": "sprites/buildings/wonders/monument_of_ages.png", + "flavor": "When the last sun sets, this stone will still be reading itself aloud.", + "flags": ["wonder"], + "encyclopedia": { "category": "civilization", "entry_type": "wonder", "detail_route": "/buildings/wonders", "tags": ["world", "culture", "tier9"] } + } +] diff --git a/src/game/engine/src/autoloads/event_bus.gd b/src/game/engine/src/autoloads/event_bus.gd index 58e9d650..b427d8c4 100644 --- a/src/game/engine/src/autoloads/event_bus.gd +++ b/src/game/engine/src/autoloads/event_bus.gd @@ -19,10 +19,6 @@ signal unit_deselected signal unit_destroyed(unit: Variant, killer: Variant) signal unit_promoted(unit: Variant, promotion: String) signal unit_healed(unit: Variant, amount: int) -signal unit_infused(unit: Variant, infusion_id: String) -signal unit_infusion_stripped(unit: Variant, infusion_id: String, stripper: Variant) -signal channeled_infusion_applied(unit: Variant, specialist: Variant, infusion_id: String) -signal channeled_infusion_faded(unit: Variant, infusion_id: String) # -- City signals -- signal city_founded(city: Variant, player_index: int) @@ -45,18 +41,9 @@ signal school_locked(player_index: int, schools: Array) signal combat_started(attacker: Variant, defender: Variant) signal combat_resolved(attacker: Variant, defender: Variant, result: Dictionary) signal combat_preview_requested(attacker: Variant, target_pos: Vector2i) -## Emitted when an Orbital Strike is queued. UI shows a warning indicator for 1 turn. -signal orbital_strike_incoming(position: Vector2i, turns_until_impact: int) - -# -- Formation signals -- -signal units_merged(formation: Variant, consumed_units: Array) -signal formation_member_lost(formation: Variant, new_count: int) # -- Item signals -- signal item_equipped(unit: Variant, item_id: String) -signal item_consumed(unit: Variant, item_id: String, charges_remaining: int) -signal item_dropped(position: Vector2i, item_id: String) -signal item_picked_up(unit: Variant, item_id: String) signal item_produced(city: Variant, item_id: String) ## Emitted when a crafted item lands in the civ Treasury (either via production queue ## completion or an apex-relic drop). Param order matches the GdCity tick caller @@ -64,55 +51,26 @@ signal item_produced(city: Variant, item_id: String) signal item_crafted(item_id: String, city: Variant, player: Variant) ## Emitted when the civ-wide material stockpile changes (add or consume). signal loot_dropped(player: Variant, creature_type: String, drops: Array) -signal stockpile_changed(player: Variant, resource_id: String, delta: int, total: int) ## Emitted when the Treasury crosses the 20-item soft cap. Informational only — ## adds are never blocked. UI should show a subtle hint, not a modal. signal treasury_soft_cap_reached(player: Variant, total: int) -# -- Keyword effect signals -- -signal battle_rage_triggered(unit: Variant) -signal arcane_shield_absorbed(unit: Variant, amount: int) - # -- Magic signals -- -signal spell_cast(spell_id: String, caster: Variant, target: Variant) signal spell_researched(spell_id: String, player_index: int) signal mana_changed(player_index: int, pool: Dictionary) signal archon_created(archon: Variant, city: Variant) -signal archon_destroyed(archon: Variant, player: Variant) -signal archon_crisis_started(player: Variant) -signal archon_choice_available(player: Variant) -signal archon_regent_appointed(player: Variant, regent_data: Dictionary) -signal mundane_rebirth_completed(player: Variant) -signal enchantment_applied(target: Variant, spell_id: String) signal enchantment_removed(target: Variant, spell_id: String) -# -- Ley line signals -- -## Emitted after ley field recompute. affected_tiles = all tiles whose mana_density changed. -signal ley_field_changed(affected_tiles: Array) -## Emitted when Doomsday Device fires — global ley collapse begins. -signal doomsday_fired(player_index: int) -signal doomsday_destroyed(player_index: int) -signal ley_collapse_started(duration_turns: int) -signal ley_collapse_ended -## Emitted when a Death-school spell corrupts a ley segment. -signal ley_segment_corrupted(segment: Array) -## Emitted when an anchor's school changes (conversion by spell or event). -## old_school is Variant (String or null for neutral anchors). -signal ley_anchor_converted(pos: Vector2i, old_school: Variant, new_school: String) - # -- Map signals -- signal tile_visibility_changed(tile: Vector2i, player_index: int, state: int) signal tile_improved(tile: Vector2i, improvement_type: String) signal tile_pillaged(tile: Vector2i) signal resources_revealed(tech_id: String, player_index: int) -signal tile_culture_flipped(tile_pos: Vector2i, old_owner: int, new_owner: int) # -- Climate signals -- signal terrain_transformed(tile: Variant, old_type: String, new_type: String) signal quality_changed(tile: Variant, old_quality: int, new_quality: int) signal wind_recalculated -signal weather_spell_cast(spell_id: String, position: Vector2i) -signal weather_spell_expired(spell_id: String) signal climate_damage_applied(unit: Variant, damage_type: String, amount: int) # -- Village/Lair signals -- @@ -120,10 +78,6 @@ signal village_discovered(tile: Vector2i, reward: Dictionary) signal lair_cleared(tile: Vector2i, reward: Dictionary) signal wild_creature_spawned(unit: Variant, lair_pos: Vector2i) -# -- Social policy signals -- -signal policy_unlock_available(player_index: int, cost: int) -signal policy_adopted(player_index: int, policy_id: String) - # -- Economy signals -- signal gold_changed(player_index: int, amount: int, total: int) signal happiness_changed(player_index: int, value: int) @@ -136,13 +90,6 @@ signal golden_age_ended(player_index: int) # -- Victory signals -- signal victory_achieved(player_index: int, victory_type: String) -signal ascension_started(player_index: int) -signal ascension_progress(player_index: int, turns_remaining: int) -signal ascension_failed(player_index: int) - -# -- Action-required signals -- -signal pending_action_focus(position: Vector2i, target: Variant) -signal pending_actions_cleared # -- Chronicle signals -- signal chronicle_opened(player_index: int) @@ -157,7 +104,6 @@ signal camera_moved(position: Vector2) signal camera_view_center_changed(center: String) signal overlay_opened(overlay_name: String) signal overlay_closed(overlay_name: String) -signal tile_hovered(axial: Vector2i) signal tile_clicked(axial: Vector2i) # -- Climate UI signals -- @@ -214,20 +160,10 @@ signal landmark_formed(pos: Vector2i, name: String, quality: int) signal natural_event_spawned(event_type: String, position: Vector2i, intensity: float) signal natural_event_moved(event_type: String, from_pos: Vector2i, to_pos: Vector2i) signal natural_event_ended(event_type: String, position: Vector2i) -signal natural_event_damage(event_type: String, target: Variant, damage: int) -signal pressure_anomaly_spawned(position: Vector2i, anomaly: float) # -- AI signals -- signal ai_turn_started(player_index: int) signal ai_turn_completed(player_index: int) -signal ai_spell_requested(spell_id: String, target: Vector2i, player_index: int) - -# -- Diplomacy signals -- -signal diplomacy_changed(a_idx: int, b_idx: int, action: String) -signal war_declared(a_idx: int, b_idx: int) -signal peace_offered(a_idx: int, b_idx: int) -signal trade_proposed(a_idx: int, b_idx: int, deal: Variant) -signal government_changed(player_index: int, old_gov: String, new_gov: String) # -- Era signals -- signal era_changed(new_era: int, player_index: int) @@ -237,17 +173,9 @@ signal improvement_started(tile: Vector2i, type: String, turns: int) signal improvement_completed(tile: Vector2i, type: String) signal improvement_removed(tile: Vector2i, type: String) -# -- Transit layer signals -- -signal unit_transited(unit: Variant, from_layer: int, to_layer: int, node_id: String) - # -- Climate / ley signals -- signal ley_network_updated(edges: Array) -# -- Bug reporting signals -- -signal crash_detected(session_data: Dictionary) -signal bug_report_submitted(report_id: String) -signal bug_report_failed(error_message: String) - # -- Arena playback signals -- ## Arena mode: emitted after all players' turns are simulated but BEFORE ## the inter-turn delay. world_map_arena.gd listens and replays the diff --git a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd index e8148705..8adf3062 100644 --- a/src/game/engine/src/modules/ai/simple_heuristic_ai.gd +++ b/src/game/engine/src/modules/ai/simple_heuristic_ai.gd @@ -59,6 +59,15 @@ static func process_player(player: RefCounted) -> Array: if bool(threat.get("threatens_city", false)) \ or int(threat.get("total_count", 0)) > mil_now: _rush_buy_defenders(player, threat) + # Early-game mil floor rush: between T40-T100, rush-buy to mil>=4 when + # gold allows. Low-production capitals (prod_total=2/turn) take ~20 turns + # per warrior, so queue-based replacement can't recover from attrition in + # time for the T100 pop5/mil4 gate. Reuses _rush_buy_defenders with a + # synthetic "threat" target of 4 so the existing cap/gold loop applies. + var t: int = GameState.turn_number + if t >= 40 and t <= 100 and mil_now < 4 and player.gold >= 50: + _rush_buy_defenders(player, {"spawn_city": null, + "threatens_city": true, "count": 3, "total_count": 3}) # Units: founders first (expansion), then military. for idx: int in player.units.size():