feat: regrow finite berry sources toward their authored cap

This commit is contained in:
Rijad Zuzo
2026-08-11 00:40:13 +02:00
parent 82e44a9f5d
commit 549a365ab2
8 changed files with 200 additions and 1 deletions
+7
View File
@@ -171,6 +171,7 @@ func simulate_tick() -> void:
print("--- Tick ", tick_count, " ---")
animal_care.advance(tick_count)
advance_player_needs()
_advance_resource_regrowth()
var village_was_changed := false
_population_view.rebuild(npcs)
for npc in npcs:
@@ -1065,6 +1066,12 @@ func advance_player_needs() -> void:
player_system.advance_needs(village.food_modifier)
func _advance_resource_regrowth() -> void:
for resource_id in resource_states:
var resource_state := resource_states[resource_id] as ResourceStateRecord
resource_state.regrow()
func get_player_state() -> PlayerStateRecord:
return player_system.player_state
+49 -1
View File
@@ -4,9 +4,10 @@ extends RefCounted
signal changed(state: ResourceStateRecord)
signal depleted(state: ResourceStateRecord)
const SCHEMA_VERSION := 3
const SCHEMA_VERSION := 4
const LEGACY_SCHEMA_VERSION := 1
const DISCOVERY_LEGACY_SCHEMA_VERSION := 2
const REGROW_LEGACY_SCHEMA_VERSION := 3
var data: Dictionary
@@ -24,6 +25,8 @@ static func create_from_node(node: ResourceNode) -> ResourceStateRecord:
"resource_id": String(node.resource_id),
"amount_remaining": node.initial_amount,
"yield_per_action": node.yield_per_action,
"max_amount": node.max_amount if node.max_amount > 0.0 else node.initial_amount,
"regrow_rate": node.regrow_rate,
"reserved_by": -1,
"enabled": node.initial_enabled,
"can_npcs_use": node.can_npcs_use,
@@ -41,6 +44,8 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
record_data = _migrate_v1(record_data)
elif version == DISCOVERY_LEGACY_SCHEMA_VERSION:
record_data = _migrate_v2(record_data)
elif version == REGROW_LEGACY_SCHEMA_VERSION:
record_data = _migrate_v3(record_data)
elif version != SCHEMA_VERSION:
return null
@@ -51,6 +56,8 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
"resource_id",
"amount_remaining",
"yield_per_action",
"max_amount",
"regrow_rate",
"reserved_by",
"enabled",
"can_npcs_use",
@@ -73,6 +80,8 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
normalized["resource_id"] = String(record_data["resource_id"])
normalized["amount_remaining"] = float(record_data["amount_remaining"])
normalized["yield_per_action"] = float(record_data["yield_per_action"])
normalized["max_amount"] = float(record_data["max_amount"])
normalized["regrow_rate"] = float(record_data["regrow_rate"])
normalized["reserved_by"] = int(record_data["reserved_by"])
normalized["enabled"] = bool(record_data["enabled"])
normalized["can_npcs_use"] = bool(record_data["can_npcs_use"])
@@ -80,6 +89,13 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
normalized["safety_risk"] = float(record_data["safety_risk"])
normalized["comfort_distance"] = float(record_data["comfort_distance"])
normalized["discovery_priority"] = float(record_data["discovery_priority"])
if (
not is_finite(normalized["max_amount"])
or normalized["max_amount"] < 0.0
or not is_finite(normalized["regrow_rate"])
or normalized["regrow_rate"] < 0.0
):
return null
return ResourceStateRecord.new(normalized)
@@ -102,6 +118,15 @@ static func _migrate_v2(legacy_data: Dictionary) -> Dictionary:
migrated["safety_risk"] = clampf(float(migrated.get("safety_risk", 0.0)), 0.0, 1.0)
migrated["comfort_distance"] = maxf(float(migrated.get("comfort_distance", 18.0)), 0.1)
migrated["discovery_priority"] = float(migrated.get("discovery_priority", 0.0))
return _migrate_v3(migrated)
static func _migrate_v3(legacy_data: Dictionary) -> Dictionary:
var migrated := legacy_data.duplicate(true)
migrated["schema_version"] = SCHEMA_VERSION
var remaining := float(migrated.get("amount_remaining", 0.0))
migrated["max_amount"] = maxf(float(migrated.get("max_amount", remaining)), 0.0)
migrated["regrow_rate"] = maxf(float(migrated.get("regrow_rate", 0.0)), 0.0)
return migrated
@@ -114,6 +139,8 @@ func apply_definition(node: ResourceNode) -> bool:
data["action_id"] = String(node.action_id)
data["resource_id"] = String(node.resource_id)
data["yield_per_action"] = node.yield_per_action
data["max_amount"] = node.max_amount if node.max_amount > 0.0 else node.initial_amount
data["regrow_rate"] = node.regrow_rate
data["can_npcs_use"] = node.can_npcs_use
data["can_player_use"] = node.can_player_use
data["safety_risk"] = node.safety_risk
@@ -151,6 +178,27 @@ func get_yield_per_action() -> float:
return float(data["yield_per_action"])
func get_max_amount() -> float:
return maxf(float(data["max_amount"]), 0.0)
func get_regrow_rate() -> float:
return maxf(float(data["regrow_rate"]), 0.0)
func regrow() -> float:
var rate := get_regrow_rate()
if rate <= 0.0 or is_equal_approx(get_amount_remaining(), get_max_amount()):
return 0.0
var previous := get_amount_remaining()
var next := minf(previous + rate, get_max_amount())
data["amount_remaining"] = next
if is_depleted():
data["reserved_by"] = -1
changed.emit(self)
return next - previous
func get_reserved_by() -> int:
return int(data["reserved_by"])
+4
View File
@@ -166,6 +166,8 @@ func _test_target_resolution_and_travel_are_separate() -> void:
"resource_id": String(SimulationIds.RESOURCE_FOOD),
"amount_remaining": 5.0,
"yield_per_action": 1.0,
"max_amount": 5.0,
"regrow_rate": 0.0,
"reserved_by": -1,
"enabled": true,
"can_npcs_use": true,
@@ -184,6 +186,8 @@ func _test_target_resolution_and_travel_are_separate() -> void:
"resource_id": String(SimulationIds.RESOURCE_FOOD),
"amount_remaining": 5.0,
"yield_per_action": 1.0,
"max_amount": 5.0,
"regrow_rate": 0.0,
"reserved_by": -1,
"enabled": true,
"can_npcs_use": true,
+2
View File
@@ -25,6 +25,8 @@ func _run() -> void:
"resource_id": String(SimulationIds.RESOURCE_FOOD),
"amount_remaining": 10.0,
"yield_per_action": 2.0,
"max_amount": 10.0,
"regrow_rate": 0.0,
"reserved_by": -1,
"enabled": true,
"can_npcs_use": true,
+132
View File
@@ -0,0 +1,132 @@
extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var manager := _create_manager(961)
var bush := ResourceNode.new()
bush.name = "RegrowBush"
bush.node_id = &"regrow_bush"
bush.action_id = SimulationIds.ACTION_GATHER_FOOD
bush.resource_id = SimulationIds.RESOURCE_FOOD
bush.initial_amount = 2.0
bush.max_amount = 5.0
bush.regrow_rate = 0.1
bush.debug_label_enabled = false
var interaction_point := Marker3D.new()
interaction_point.name = "InteractionPoint"
interaction_point.position = Vector3.ZERO
bush.add_child(interaction_point)
root.add_child(bush)
await process_frame
_check(
manager.register_resource_node(bush), "Regrowth test should bind a real finite ResourceNode"
)
var state: ResourceStateRecord = manager.get_resource_state(bush.node_id)
_check(
(
is_equal_approx(state.get_amount_remaining(), 2.0)
and is_equal_approx(state.get_max_amount(), 5.0)
and is_equal_approx(state.get_regrow_rate(), 0.1)
),
"Regrowth metadata should bind from the authored node"
)
_check(
is_equal_approx(manager.harvest_resource_node(bush), 2.0),
"Harvesting should deplete the regrowing bush into carried inventory"
)
_check(
is_equal_approx(state.get_amount_remaining(), 0.0),
"The regrowing bush should reach zero after harvesting"
)
manager.simulate_tick()
manager.simulate_tick()
_check(
is_equal_approx(state.get_amount_remaining(), 0.2),
"Regrowing resources should recover exactly their rate per simulation tick"
)
for _tick in 50:
manager.simulate_tick()
_check(
(
is_equal_approx(state.get_amount_remaining(), 5.0)
and state.get_amount_remaining() <= state.get_max_amount()
),
"Regrowth should stop at the authored maximum without creating extra food"
)
manager.simulate_tick()
var saved_json: String = manager.serialize_state()
var restored := _create_manager(962)
_check(
restored.restore_state_from_json(saved_json),
"Regrowing resource state should survive save and restore"
)
_check(
(
restored.get_state_checksum() == manager.get_state_checksum()
and is_equal_approx(
restored.get_resource_state(bush.node_id).get_amount_remaining(), 5.0
)
and is_equal_approx(restored.get_resource_state(bush.node_id).get_regrow_rate(), 0.1)
),
"Restore should preserve the regrow amount, cap, and checksum"
)
restored.free()
var v3_data: Dictionary = state.to_dictionary()
v3_data["schema_version"] = ResourceStateRecord.REGROW_LEGACY_SCHEMA_VERSION
v3_data.erase("max_amount")
v3_data.erase("regrow_rate")
var migrated := ResourceStateRecord.from_dictionary(v3_data)
_check(
(
migrated != null
and is_equal_approx(migrated.get_max_amount(), 5.0)
and is_equal_approx(migrated.get_regrow_rate(), 0.0)
),
"Resource v3 should migrate with its current amount as cap and no regrowth"
)
bush.queue_free()
manager.free()
_finish()
func _create_manager(seed_value: int = 961) -> 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] Resource regrowth passed: bounded recovery -> cap -> restore")
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)
+1
View File
@@ -0,0 +1 @@
uid://bxxet5dkxrk1p
+3
View File
@@ -490,6 +490,7 @@ position = Vector3(-8, 0, -17)
[node name="BerryBush_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(-6, 0, -8)
node_id = &"berry_bush_01"
regrow_rate = 0.02
safety_risk = 0.05
comfort_distance = 18.0
discovery_priority = 1.0
@@ -498,6 +499,7 @@ discovery_priority = 1.0
position = Vector3(-10, 0, -4)
node_id = &"berry_bush_02"
initial_amount = 8.0
regrow_rate = 0.02
safety_risk = 0.08
comfort_distance = 20.0
discovery_priority = 0.5
@@ -507,6 +509,7 @@ position = Vector3(12, 0, 10)
node_id = &"berry_patch_river_01"
initial_amount = 6.0
yield_per_action = 1.5
regrow_rate = 0.02
safety_risk = 0.18
comfort_distance = 22.0
discovery_priority = 0.25
+2
View File
@@ -12,6 +12,8 @@ static var _all: Array[ResourceNode] = []
@export var resource_id: StringName = SimulationIds.RESOURCE_FOOD
@export var initial_amount: float = 10.0
@export var yield_per_action: float = 2.0
@export var max_amount: float = 0.0
@export var regrow_rate: float = 0.0
@export var initial_enabled: bool = true
@export var can_npcs_use: bool = true
@export var can_player_use: bool = true