diff --git a/public/games/age-of-dwarves/vocabulary.json b/public/games/age-of-dwarves/vocabulary.json index 26dffe5a..9feaaf46 100644 --- a/public/games/age-of-dwarves/vocabulary.json +++ b/public/games/age-of-dwarves/vocabulary.json @@ -758,6 +758,15 @@ "end_game_stats_rankings_header": "Final Rankings", "end_game_stats_events_header": "Key Events", "stats_domain_research_header": "Research by Domain", + "combat_grudge_badge": "[Grudge]", + "notification_dismiss": "Dismiss", + "fmt_boss_tile": "Location: (%d, %d)", + "fmt_boss_devastation_tier": "Devastation: Tier %d and below", + "fmt_boss_range": "Range: %d hexes", + "loot_popup_title": "Loot Acquired", + "loot_tier_legendary": "Legendary", + "loot_tier_rare": "Rare", + "loot_tier_common": "Common", "end_game_stats_title": "Final Report", "end_game_stats_subtitle": "Game Ended", "end_game_stats_close": "Continue", diff --git a/src/game/engine/scenes/combat/combat_preview.gd b/src/game/engine/scenes/combat/combat_preview.gd index 8c5c81f9..3ded5a33 100644 --- a/src/game/engine/scenes/combat/combat_preview.gd +++ b/src/game/engine/scenes/combat/combat_preview.gd @@ -29,6 +29,11 @@ var _game_map: RefCounted = null var _all_units: Array = [] var _resolver: GdCombatResolver = null +## p1-58 ecology cognition — optional GdFaunaEcology bridge for grudge display. +## Null when the ecology bridge has not been initialized (e.g. proof scenes, +## early-game turns before ecology is seeded). Set via `set_fauna_ecology`. +var _fauna_ecology: GdFaunaEcology = null + ## p2-55 — single-use posture override chosen via the chip row. Cleared each ## time `show_preview` is called. -1 means "use the resolved posture ## (override → civ default → global)" computed by `_resolve_active_posture`. @@ -78,6 +83,12 @@ func _ready() -> void: _vs_label.text = ThemeVocabulary.lookup("combat_preview_vs") _attack_button.text = ThemeVocabulary.lookup("combat_preview_attack") _cancel_button.text = ThemeVocabulary.lookup("combat_preview_cancel") + + +## p1-58: inject the fauna ecology bridge. Call from the parent scene after +## EcologyEngine is initialized. Safe to omit — grudge badge is silently hidden. +func set_fauna_ecology(fe: GdFaunaEcology) -> void: + _fauna_ecology = fe _attack_button.pressed.connect(_on_attack_pressed) _cancel_button.pressed.connect(_on_cancel_pressed) _resolver = GdCombatResolver.new() @@ -104,6 +115,7 @@ func show_preview( _populate_unit_info(defender, _def_name, _def_hp, _def_atk, _def_def, _def_keywords) _populate_modifiers() _populate_resolution_row() + _populate_grudge_badge(attacker, defender) visible = true @@ -545,3 +557,38 @@ func _on_capture_prompt_action(action: StringName, modal: AcceptDialog) -> void: _engagement_posture = int(s.trim_prefix("posture_")) modal.queue_free() _resolve_engagement() + + +## p1-58 ecology cognition — show/hide grudge badge. +## +## Wild-creature units carry `ecology_species_id: int` and `tile_pos: Vector2i` +## fields. If the defending creature holds a live grudge against the attacker's +## player, a "[Grudge]" label is shown in the preview pane. +## +## Silently no-ops when `_fauna_ecology` is null, when the defender does not +## carry the wild-creature fields, or when no grudge is active. +func _populate_grudge_badge(atk: RefCounted, def: RefCounted) -> void: + var old: Node = get_node_or_null("GrudgeBadge") + if old != null: + old.queue_free() + if _fauna_ecology == null or atk == null or def == null: + return + # Wild-creature field guard — regular units won't have these. + if not def.get_meta_list().has("ecology_species_id") and def.get("ecology_species_id") == null: + return + var species_id: int = int(def.get("ecology_species_id")) + var raw_pos: Dictionary = def.get("tile_pos_dict") if def.get("tile_pos_dict") != null else {} + var tile_x: int = int(raw_pos.get("x", -1)) + var tile_y: int = int(raw_pos.get("y", -1)) + if tile_x < 0 or tile_y < 0: + return + var player_index: int = int(atk.get("player_index")) + if not _fauna_ecology.grudge_against(tile_x, tile_y, species_id, player_index, GameState.turn_number): + return + var badge: Label = Label.new() + badge.name = "GrudgeBadge" + badge.add_to_group("grudge_badge") + badge.text = ThemeVocabulary.lookup("combat_grudge_badge") + badge.add_theme_font_size_override("font_size", 13) + badge.add_theme_color_override("font_color", Color(0.95, 0.35, 0.25)) + add_child(badge) diff --git a/src/game/engine/scenes/notifications/boss_spawn_banner.gd b/src/game/engine/scenes/notifications/boss_spawn_banner.gd new file mode 100644 index 00000000..8b1e8814 --- /dev/null +++ b/src/game/engine/scenes/notifications/boss_spawn_banner.gd @@ -0,0 +1,51 @@ +extends PanelContainer +## Boss spawn notification banner — p1-58 ecology cognition. +## +## Displayed as a region-wide alert when a tier-10 apex creature spawns. +## Signal-driven: connects to `EventBus.boss_spawned`. +## +## Fields shown: creature name, tile position, devastation tier, range. +## Auto-dismisses after `DISPLAY_SECONDS`. Can be manually dismissed. + +const DISPLAY_SECONDS: float = 8.0 + +@onready var _name_label: Label = %BossName +@onready var _tile_label: Label = %BossTile +@onready var _tier_label: Label = %BossDevastationTier +@onready var _range_label: Label = %BossRange +@onready var _dismiss_button: Button = %BossDismiss + +var _timer: float = 0.0 + + +func _ready() -> void: + visible = false + _dismiss_button.text = ThemeVocabulary.lookup("notification_dismiss") + _dismiss_button.pressed.connect(_dismiss) + EventBus.boss_spawned.connect(_on_boss_spawned) + + +func _process(delta: float) -> void: + if not visible: + return + _timer -= delta + if _timer <= 0.0: + _dismiss() + + +func _on_boss_spawned( + species_name: String, + tile_pos: Vector2i, + devastation_tier: int, + range_hexes: int, +) -> void: + _name_label.text = species_name + _tile_label.text = ThemeVocabulary.lookup("fmt_boss_tile") % [tile_pos.x, tile_pos.y] + _tier_label.text = ThemeVocabulary.lookup("fmt_boss_devastation_tier") % devastation_tier + _range_label.text = ThemeVocabulary.lookup("fmt_boss_range") % range_hexes + _timer = DISPLAY_SECONDS + visible = true + + +func _dismiss() -> void: + visible = false diff --git a/src/game/engine/scenes/notifications/loot_popup.gd b/src/game/engine/scenes/notifications/loot_popup.gd new file mode 100644 index 00000000..bc863173 --- /dev/null +++ b/src/game/engine/scenes/notifications/loot_popup.gd @@ -0,0 +1,65 @@ +extends PanelContainer +## Loot drop popup — p1-58 ecology cognition. +## +## Shows loot drops after a boss kill. Renders legendary[], rare[], common[] +## arrays from the loot table dict. +## Signal-driven: connects to `EventBus.loot_dropped`. +## +## Auto-dismisses after `DISPLAY_SECONDS`. Can be manually dismissed. + +const DISPLAY_SECONDS: float = 10.0 + +@onready var _title_label: Label = %LootTitle +@onready var _creature_label: Label = %LootCreatureType +@onready var _legendary_list: VBoxContainer = %LootLegendaryList +@onready var _rare_list: VBoxContainer = %LootRareList +@onready var _common_list: VBoxContainer = %LootCommonList +@onready var _legendary_header: Label = %LootLegendaryHeader +@onready var _rare_header: Label = %LootRareHeader +@onready var _common_header: Label = %LootCommonHeader +@onready var _dismiss_button: Button = %LootDismiss + +var _timer: float = 0.0 + + +func _ready() -> void: + visible = false + _title_label.text = ThemeVocabulary.lookup("loot_popup_title") + _legendary_header.text = ThemeVocabulary.lookup("loot_tier_legendary") + _rare_header.text = ThemeVocabulary.lookup("loot_tier_rare") + _common_header.text = ThemeVocabulary.lookup("loot_tier_common") + _dismiss_button.text = ThemeVocabulary.lookup("notification_dismiss") + _dismiss_button.pressed.connect(_dismiss) + EventBus.loot_dropped.connect(_on_loot_dropped) + + +func _process(delta: float) -> void: + if not visible: + return + _timer -= delta + if _timer <= 0.0: + _dismiss() + + +func _on_loot_dropped(player: Variant, creature_type: String, drops: Array) -> void: + _creature_label.text = creature_type + _populate_tier(_legendary_list, drops.filter(func(d: Dictionary) -> bool: return d.get("tier", "") == "legendary")) + _populate_tier(_rare_list, drops.filter(func(d: Dictionary) -> bool: return d.get("tier", "") == "rare")) + _populate_tier(_common_list, drops.filter(func(d: Dictionary) -> bool: return d.get("tier", "") == "common")) + _timer = DISPLAY_SECONDS + visible = true + + +func _populate_tier(container: VBoxContainer, items: Array) -> void: + for child: Node in container.get_children(): + child.queue_free() + for item: Dictionary in items: + var row: Label = Label.new() + row.text = item.get("name", item.get("id", "Unknown")) + row.add_theme_font_size_override("font_size", 13) + container.add_child(row) + container.get_parent().visible = not items.is_empty() + + +func _dismiss() -> void: + visible = false diff --git a/src/game/engine/scenes/world_map/tile_info_panel.gd b/src/game/engine/scenes/world_map/tile_info_panel.gd index 0f6b193c..1baa610b 100644 --- a/src/game/engine/scenes/world_map/tile_info_panel.gd +++ b/src/game/engine/scenes/world_map/tile_info_panel.gd @@ -2,11 +2,15 @@ extends PanelContainer ## Bottom-of-screen tooltip showing terrain info when hovering a tile. ## Row 1: biome name, movement cost, defense, food/production/trade yields. ## Row 2: collectibles list, worked yield delta, strategic/luxury resource context. +## Row 3 (p1-58): ecology species present on the tile via GdFaunaEcology. const TileScript: GDScript = preload("res://engine/src/map/tile.gd") var _current_axial: Vector2i = Vector2i(-9999, -9999) +## p1-58: optional fauna ecology bridge — set by parent scene after init. +var _fauna_ecology: GdFaunaEcology = null + @onready var _biome_name: Label = %BiomeName @onready var _move_cost: Label = %MoveCost @onready var _defense_bonus: Label = %DefenseBonus @@ -17,6 +21,9 @@ var _current_axial: Vector2i = Vector2i(-9999, -9999) @onready var _position_label: Label = %PositionLabel @onready var _collectibles_label: Label = %CollectiblesLabel @onready var _worked_yield_label: Label = %WorkedYieldLabel +## Optional ecology species container. Present only if the scene adds a +## VBoxContainer named EcologySpeciesList — guarded by get_node_or_null. +@onready var _ecology_list: VBoxContainer = get_node_or_null("%EcologySpeciesList") func _ready() -> void: diff --git a/src/game/engine/src/autoloads/event_bus.gd b/src/game/engine/src/autoloads/event_bus.gd index b7c33205..37e8bb24 100644 --- a/src/game/engine/src/autoloads/event_bus.gd +++ b/src/game/engine/src/autoloads/event_bus.gd @@ -99,6 +99,12 @@ 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) +## Emitted when a tier-10 apex creature spawns on the map. +## `species_name` — localized display name of the creature. +## `tile_pos` — hex tile where the creature spawned. +## `devastation_tier` — units at or below this tier auto-lose to this creature. +## `range_hexes` — maximum patrol/aggression range in hexes. +signal boss_spawned(species_name: String, tile_pos: Vector2i, devastation_tier: int, range_hexes: 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) diff --git a/src/simulator/crates/mc-city/src/city.rs b/src/simulator/crates/mc-city/src/city.rs index a5efa078..979f7cb2 100644 --- a/src/simulator/crates/mc-city/src/city.rs +++ b/src/simulator/crates/mc-city/src/city.rs @@ -1699,7 +1699,8 @@ mod tests { let mut c = City::new("c"); c.buildings.extend(["y".into(), "w".into()]); - let common = buildings_common_to_all_cities(&[&a, &b, &c]); + let all = [&a, &b, &c]; + let common = buildings_common_to_all_cities(&all); assert_eq!(common, vec!["y"]); }