From e51cde57bef1fe54a884907738ef0afc68f30f06 Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Tue, 11 Aug 2026 00:43:48 +0200 Subject: [PATCH] feat: add cold-season scarcity that halves yields and pauses regrowth --- simulation/SimulationManager.gd | 28 ++++++- tests/seasonal_rhythm_test.gd | 127 ++++++++++++++++++++++++++++++ tests/seasonal_rhythm_test.gd.uid | 1 + world/ui/ui.gd | 3 + 4 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tests/seasonal_rhythm_test.gd create mode 100644 tests/seasonal_rhythm_test.gd.uid diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index 64f62cf..8234452 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -68,6 +68,10 @@ var _population_view := SimulationPopulationView.new() var speed_index := 2 const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0] const KNOWLEDGE_COMMUNICATION_RADIUS := 2.5 +const SEASON_DAYS := 4 +const COLD_DAYS := 3 +const SEASON_COLD := &"cold" +const SEASON_WARM := &"warm" signal speed_changed(multiplier: float) const NPC_NAMES := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"] @@ -337,7 +341,9 @@ func _complete_resource_gather(npc: SimNPC, completed_task: StringName) -> void: release_npc_reservation(npc.id) return - var extracted := resource_state.extract() + var extracted := resource_state.extract( + resource_state.get_yield_per_action() * _gather_yield_multiplier() + ) if extracted > 0.0: var resource_id := resource_state.get_resource_id() npc.add_inventory(resource_id, extracted) @@ -1019,7 +1025,9 @@ func harvest_resource_node(node: ResourceNode) -> float: var resource_state := get_resource_state(node.node_id) if resource_state == null or not resource_state.can_player_use_resource(): return 0.0 - var extracted := resource_state.extract(resource_state.get_yield_per_action()) + var extracted := resource_state.extract( + resource_state.get_yield_per_action() * _gather_yield_multiplier() + ) if extracted <= 0.0: return 0.0 var resource_id := resource_state.get_resource_id() @@ -1066,7 +1074,23 @@ func advance_player_needs() -> void: player_system.advance_needs(village.food_modifier) +func get_season() -> StringName: + var ticks_per_day := maxi(get_knowledge_review_interval(), 1) + var season_day := int(clock.elapsed_ticks / ticks_per_day) + return SEASON_COLD if season_day % SEASON_DAYS >= COLD_DAYS else SEASON_WARM + + +func is_cold_season() -> bool: + return get_season() == SEASON_COLD + + +func _gather_yield_multiplier() -> float: + return 0.5 if is_cold_season() else 1.0 + + func _advance_resource_regrowth() -> void: + if is_cold_season(): + return for resource_id in resource_states: var resource_state := resource_states[resource_id] as ResourceStateRecord resource_state.regrow() diff --git a/tests/seasonal_rhythm_test.gd b/tests/seasonal_rhythm_test.gd new file mode 100644 index 0000000..b00dc83 --- /dev/null +++ b/tests/seasonal_rhythm_test.gd @@ -0,0 +1,127 @@ +extends SceneTree + +var failures: Array[String] = [] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var manager := _create_manager(971) + var ticks_per_day: int = manager.get_knowledge_review_interval() + + var berry := ResourceNode.new() + berry.name = "SeasonBerry" + berry.node_id = &"season_berry" + berry.action_id = SimulationIds.ACTION_GATHER_FOOD + berry.resource_id = SimulationIds.RESOURCE_FOOD + berry.initial_amount = 2.0 + berry.max_amount = 2.0 + berry.regrow_rate = 0.1 + berry.debug_label_enabled = false + var interaction_point := Marker3D.new() + interaction_point.name = "InteractionPoint" + interaction_point.position = Vector3.ZERO + berry.add_child(interaction_point) + root.add_child(berry) + await process_frame + _check( + manager.register_resource_node(berry), "Season test should bind a real finite ResourceNode" + ) + var state: ResourceStateRecord = manager.get_resource_state(berry.node_id) + + manager.clock.elapsed_ticks = 0 * ticks_per_day + 50 + _check( + ( + manager.get_season() == &"warm" + and is_equal_approx(manager._gather_yield_multiplier(), 1.0) + ), + "The first season days should be warm with full yields" + ) + _check( + is_equal_approx(manager.harvest_resource_node(berry), 2.0), + "Warm-season gathering should extract the full configured yield" + ) + + state.set_amount_remaining(2.0) + manager.clock.elapsed_ticks = 3 * ticks_per_day + 50 + _check( + ( + manager.get_season() == &"cold" + and is_equal_approx(manager._gather_yield_multiplier(), 0.5) + ), + "Later season days should be cold with reduced yields" + ) + _check( + is_equal_approx(manager.harvest_resource_node(berry), 1.0), + "Cold-season gathering should extract only half the configured yield" + ) + + state.set_amount_remaining(0.5) + manager.simulate_tick() + _check( + is_equal_approx(state.get_amount_remaining(), 0.5), + "Cold-season regrowth should pause until warmer days" + ) + manager.clock.elapsed_ticks = 4 * ticks_per_day + 50 + _check(manager.get_season() == &"warm", "The season should return warm after the cold spell") + manager.simulate_tick() + _check( + is_equal_approx(state.get_amount_remaining(), 0.6), + "Regrowth should resume once the warm season returns" + ) + + manager.clock.elapsed_ticks = 3 * ticks_per_day + 50 + var saved_json: String = manager.serialize_state() + var restored := _create_manager(972) + _check( + restored.restore_state_from_json(saved_json), + "Seasonal clock state should survive save and restore" + ) + _check( + ( + restored.get_state_checksum() == manager.get_state_checksum() + and restored.get_season() == &"cold" + and is_equal_approx(restored._gather_yield_multiplier(), 0.5) + ), + "Restore should re-derive the same cold season and its reduced yields" + ) + restored.free() + + berry.queue_free() + manager.free() + _finish() + + +func _create_manager(seed_value: int = 971) -> Node: + var manager: Node = load("res://simulation/SimulationManager.gd").new() + manager.simulation_seed = seed_value + manager.debug_logs = false + var home_positions: Array[Vector3] = [ + Vector3(0.0, 0.0, 0.0), + Vector3(2.0, 0.0, 0.0), + Vector3(10.0, 0.0, 0.0), + Vector3(12.0, 0.0, 0.0), + Vector3(30.0, 0.0, 30.0), + Vector3(40.0, 0.0, 40.0), + ] + manager.home_positions = home_positions + root.add_child(manager) + manager.set_process(false) + return manager + + +func _finish() -> void: + if failures.is_empty(): + print("[TEST] Seasonal rhythm passed: cold yields + paused regrowth") + quit(0) + return + for failure in failures: + push_error("[TEST] " + failure) + quit(1) + + +func _check(condition: bool, message: String) -> void: + if not condition: + failures.append(message) diff --git a/tests/seasonal_rhythm_test.gd.uid b/tests/seasonal_rhythm_test.gd.uid new file mode 100644 index 0000000..1f001e3 --- /dev/null +++ b/tests/seasonal_rhythm_test.gd.uid @@ -0,0 +1 @@ +uid://bu0iqfwmmcwsk diff --git a/world/ui/ui.gd b/world/ui/ui.gd index 92b4b2b..d39ba66 100644 --- a/world/ui/ui.gd +++ b/world/ui/ui.gd @@ -86,6 +86,7 @@ func _on_village_changed(village: SimVillage) -> void: if rates["wood_per_day"] > 0.01: wood_rate = " (−%.0f/d)" % rates["wood_per_day"] var opportunity_text := _build_active_opportunity_display() + var season_text := "Cold" if simulation_manager.is_cold_season() else "Warm" village_stats_label.text = ( """ Village [%s] @@ -95,6 +96,7 @@ func _on_village_changed(village: SimVillage) -> void: Safety: %s Knowledge: %s Starving: %s + Season: %s Food Mod: %.2f Safety Mod: %.2f @@ -110,6 +112,7 @@ func _on_village_changed(village: SimVillage) -> void: round(village.safety), round(village.knowledge), simulation_manager.get_starving_count(), + season_text, village.food_modifier, village.safety_modifier, village.knowledge_modifier,