diff --git a/src/game/engine/scenes/tests/proof_biome_economy_coupling.gd b/src/game/engine/scenes/tests/proof_biome_economy_coupling.gd index 6fa3a00f..551ec5ce 100644 --- a/src/game/engine/scenes/tests/proof_biome_economy_coupling.gd +++ b/src/game/engine/scenes/tests/proof_biome_economy_coupling.gd @@ -132,6 +132,14 @@ func _run_happiness_pipeline() -> void: for pos: Vector2i in OWNED_TILES: owned_pairs.append([int(pos.x), int(pos.y)]) var products: Array = _build_products_array() + print("Products from DataLoader (with source_fauna): %d" % products.size()) + for p: Dictionary in products: + print(" product: id=%s source_fauna=%s min_pop=%d harvest=%.2f" % [ + p.get("id", "?"), + str(p.get("source_fauna", [])), + int(p.get("min_population", 0)), + float(p.get("harvest_rate", 0.0)), + ]) if _fauna_eco != null: _supply = _fauna_eco.call( "fauna_product_supply", diff --git a/src/game/engine/src/autoloads/audio_manager.gd b/src/game/engine/src/autoloads/audio_manager.gd index 6d88b930..17bf1269 100644 --- a/src/game/engine/src/autoloads/audio_manager.gd +++ b/src/game/engine/src/autoloads/audio_manager.gd @@ -27,6 +27,7 @@ var _crossfade_seconds: float = 2.0 ## value is an array of track IDs from `_music_tracks`. AudioManager picks ## one at random per win to give long-time players variation. var _victory_pool: Dictionary = {} +var _defeat_pool: Dictionary = {} var _sfx_pool: Array[AudioStreamPlayer] = [] var _sfx_cursor: int = 0 @@ -98,6 +99,7 @@ func load_theme(theme_id: String) -> void: continue _music_tracks[id] = track _victory_pool = (music.get("victory_pool", {}) as Dictionary).duplicate() + _defeat_pool = (music.get("defeat_pool", {}) as Dictionary).duplicate() _loaded = true @@ -602,9 +604,16 @@ func _on_unit_moved(_unit: Variant, _from: Vector2i, _to: Vector2i) -> void: play_sfx("unit_moved") -func _on_victory_achieved(_player_index: int, victory_type: String) -> void: - play_sfx("victory_fanfare") - play_music(_pick_victory_track(victory_type)) +func _on_victory_achieved(player_index: int, victory_type: String) -> void: + # A win is the listener's win only if the winner is the local human. + # Otherwise the human is being defeated by this winner's strategy and + # we play the matching defeat-by- track. + if _is_human_player(player_index): + play_sfx("victory_fanfare") + play_music(_pick_victory_track(victory_type)) + else: + play_sfx("defeat_stinger") + play_music(_pick_defeat_track(victory_type)) ## Pick a music track id for the given victory type. Looks the type up in @@ -622,15 +631,41 @@ func _pick_victory_track(victory_type: String) -> String: return _music_default_id +## Mirror of _pick_victory_track for `defeat_pool`. Returns a defeat track +## id keyed to *how* the human player was defeated. Falls back to the +## generic "defeat" track when the victory_type is unmapped. +func _pick_defeat_track(victory_type: String) -> String: + if _defeat_pool.has(victory_type) and _defeat_pool[victory_type] is Array: + var pool: Array = _defeat_pool[victory_type] as Array + if pool.size() > 0: + return String(pool[_rng.randi_range(0, pool.size() - 1)]) + if _music_tracks.has("defeat"): + return "defeat" + return _music_default_id + + +## Helper: is `player_index` the local human player? Returns false on +## out-of-range indices and on players that don't expose `is_human`. +func _is_human_player(player_index: int) -> bool: + if player_index < 0 or player_index >= GameState.players.size(): + return false + var player: RefCounted = GameState.players[player_index] as RefCounted + if player == null or not ("is_human" in player): + return false + return bool(player.get("is_human")) + + ## Defeat is the human-player counterpart of victory_achieved. The signal ## fires for any eliminated player; we only swap to defeat audio when the ## eliminated player is the local human, otherwise the listener gets ## defeat music for an AI's loss which is wrong. func _on_player_eliminated(player_index: int) -> void: - if player_index < 0 or player_index >= GameState.players.size(): - return - var player: RefCounted = GameState.players[player_index] as RefCounted - if player == null or not ("is_human" in player) or not bool(player.get("is_human")): + # This signal carries no victory_type — fires for last-unit-destroyed + # eliminations etc. When the elimination is also a victory_achieved + # (an AI just won), that handler already swapped to the defeat-by-X + # track via _pick_defeat_track; re-asserting the generic "defeat" + # here is harmless (same Music bus, crossfade tweens). + if not _is_human_player(player_index): return play_sfx("defeat_stinger") play_music("defeat")