feat: regrow finite berry sources toward their authored cap
This commit is contained in:
@@ -171,6 +171,7 @@ func simulate_tick() -> void:
|
|||||||
print("--- Tick ", tick_count, " ---")
|
print("--- Tick ", tick_count, " ---")
|
||||||
animal_care.advance(tick_count)
|
animal_care.advance(tick_count)
|
||||||
advance_player_needs()
|
advance_player_needs()
|
||||||
|
_advance_resource_regrowth()
|
||||||
var village_was_changed := false
|
var village_was_changed := false
|
||||||
_population_view.rebuild(npcs)
|
_population_view.rebuild(npcs)
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
@@ -1065,6 +1066,12 @@ func advance_player_needs() -> void:
|
|||||||
player_system.advance_needs(village.food_modifier)
|
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:
|
func get_player_state() -> PlayerStateRecord:
|
||||||
return player_system.player_state
|
return player_system.player_state
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ extends RefCounted
|
|||||||
signal changed(state: ResourceStateRecord)
|
signal changed(state: ResourceStateRecord)
|
||||||
signal depleted(state: ResourceStateRecord)
|
signal depleted(state: ResourceStateRecord)
|
||||||
|
|
||||||
const SCHEMA_VERSION := 3
|
const SCHEMA_VERSION := 4
|
||||||
const LEGACY_SCHEMA_VERSION := 1
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
const DISCOVERY_LEGACY_SCHEMA_VERSION := 2
|
const DISCOVERY_LEGACY_SCHEMA_VERSION := 2
|
||||||
|
const REGROW_LEGACY_SCHEMA_VERSION := 3
|
||||||
|
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ static func create_from_node(node: ResourceNode) -> ResourceStateRecord:
|
|||||||
"resource_id": String(node.resource_id),
|
"resource_id": String(node.resource_id),
|
||||||
"amount_remaining": node.initial_amount,
|
"amount_remaining": node.initial_amount,
|
||||||
"yield_per_action": node.yield_per_action,
|
"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,
|
"reserved_by": -1,
|
||||||
"enabled": node.initial_enabled,
|
"enabled": node.initial_enabled,
|
||||||
"can_npcs_use": node.can_npcs_use,
|
"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)
|
record_data = _migrate_v1(record_data)
|
||||||
elif version == DISCOVERY_LEGACY_SCHEMA_VERSION:
|
elif version == DISCOVERY_LEGACY_SCHEMA_VERSION:
|
||||||
record_data = _migrate_v2(record_data)
|
record_data = _migrate_v2(record_data)
|
||||||
|
elif version == REGROW_LEGACY_SCHEMA_VERSION:
|
||||||
|
record_data = _migrate_v3(record_data)
|
||||||
elif version != SCHEMA_VERSION:
|
elif version != SCHEMA_VERSION:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -51,6 +56,8 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
|
|||||||
"resource_id",
|
"resource_id",
|
||||||
"amount_remaining",
|
"amount_remaining",
|
||||||
"yield_per_action",
|
"yield_per_action",
|
||||||
|
"max_amount",
|
||||||
|
"regrow_rate",
|
||||||
"reserved_by",
|
"reserved_by",
|
||||||
"enabled",
|
"enabled",
|
||||||
"can_npcs_use",
|
"can_npcs_use",
|
||||||
@@ -73,6 +80,8 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
|
|||||||
normalized["resource_id"] = String(record_data["resource_id"])
|
normalized["resource_id"] = String(record_data["resource_id"])
|
||||||
normalized["amount_remaining"] = float(record_data["amount_remaining"])
|
normalized["amount_remaining"] = float(record_data["amount_remaining"])
|
||||||
normalized["yield_per_action"] = float(record_data["yield_per_action"])
|
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["reserved_by"] = int(record_data["reserved_by"])
|
||||||
normalized["enabled"] = bool(record_data["enabled"])
|
normalized["enabled"] = bool(record_data["enabled"])
|
||||||
normalized["can_npcs_use"] = bool(record_data["can_npcs_use"])
|
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["safety_risk"] = float(record_data["safety_risk"])
|
||||||
normalized["comfort_distance"] = float(record_data["comfort_distance"])
|
normalized["comfort_distance"] = float(record_data["comfort_distance"])
|
||||||
normalized["discovery_priority"] = float(record_data["discovery_priority"])
|
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)
|
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["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["comfort_distance"] = maxf(float(migrated.get("comfort_distance", 18.0)), 0.1)
|
||||||
migrated["discovery_priority"] = float(migrated.get("discovery_priority", 0.0))
|
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
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
@@ -114,6 +139,8 @@ func apply_definition(node: ResourceNode) -> bool:
|
|||||||
data["action_id"] = String(node.action_id)
|
data["action_id"] = String(node.action_id)
|
||||||
data["resource_id"] = String(node.resource_id)
|
data["resource_id"] = String(node.resource_id)
|
||||||
data["yield_per_action"] = node.yield_per_action
|
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_npcs_use"] = node.can_npcs_use
|
||||||
data["can_player_use"] = node.can_player_use
|
data["can_player_use"] = node.can_player_use
|
||||||
data["safety_risk"] = node.safety_risk
|
data["safety_risk"] = node.safety_risk
|
||||||
@@ -151,6 +178,27 @@ func get_yield_per_action() -> float:
|
|||||||
return float(data["yield_per_action"])
|
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:
|
func get_reserved_by() -> int:
|
||||||
return int(data["reserved_by"])
|
return int(data["reserved_by"])
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ func _test_target_resolution_and_travel_are_separate() -> void:
|
|||||||
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
||||||
"amount_remaining": 5.0,
|
"amount_remaining": 5.0,
|
||||||
"yield_per_action": 1.0,
|
"yield_per_action": 1.0,
|
||||||
|
"max_amount": 5.0,
|
||||||
|
"regrow_rate": 0.0,
|
||||||
"reserved_by": -1,
|
"reserved_by": -1,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"can_npcs_use": true,
|
"can_npcs_use": true,
|
||||||
@@ -184,6 +186,8 @@ func _test_target_resolution_and_travel_are_separate() -> void:
|
|||||||
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
||||||
"amount_remaining": 5.0,
|
"amount_remaining": 5.0,
|
||||||
"yield_per_action": 1.0,
|
"yield_per_action": 1.0,
|
||||||
|
"max_amount": 5.0,
|
||||||
|
"regrow_rate": 0.0,
|
||||||
"reserved_by": -1,
|
"reserved_by": -1,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"can_npcs_use": true,
|
"can_npcs_use": true,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ func _run() -> void:
|
|||||||
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
||||||
"amount_remaining": 10.0,
|
"amount_remaining": 10.0,
|
||||||
"yield_per_action": 2.0,
|
"yield_per_action": 2.0,
|
||||||
|
"max_amount": 10.0,
|
||||||
|
"regrow_rate": 0.0,
|
||||||
"reserved_by": -1,
|
"reserved_by": -1,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"can_npcs_use": true,
|
"can_npcs_use": true,
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bxxet5dkxrk1p
|
||||||
@@ -490,6 +490,7 @@ position = Vector3(-8, 0, -17)
|
|||||||
[node name="BerryBush_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
[node name="BerryBush_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
|
||||||
position = Vector3(-6, 0, -8)
|
position = Vector3(-6, 0, -8)
|
||||||
node_id = &"berry_bush_01"
|
node_id = &"berry_bush_01"
|
||||||
|
regrow_rate = 0.02
|
||||||
safety_risk = 0.05
|
safety_risk = 0.05
|
||||||
comfort_distance = 18.0
|
comfort_distance = 18.0
|
||||||
discovery_priority = 1.0
|
discovery_priority = 1.0
|
||||||
@@ -498,6 +499,7 @@ discovery_priority = 1.0
|
|||||||
position = Vector3(-10, 0, -4)
|
position = Vector3(-10, 0, -4)
|
||||||
node_id = &"berry_bush_02"
|
node_id = &"berry_bush_02"
|
||||||
initial_amount = 8.0
|
initial_amount = 8.0
|
||||||
|
regrow_rate = 0.02
|
||||||
safety_risk = 0.08
|
safety_risk = 0.08
|
||||||
comfort_distance = 20.0
|
comfort_distance = 20.0
|
||||||
discovery_priority = 0.5
|
discovery_priority = 0.5
|
||||||
@@ -507,6 +509,7 @@ position = Vector3(12, 0, 10)
|
|||||||
node_id = &"berry_patch_river_01"
|
node_id = &"berry_patch_river_01"
|
||||||
initial_amount = 6.0
|
initial_amount = 6.0
|
||||||
yield_per_action = 1.5
|
yield_per_action = 1.5
|
||||||
|
regrow_rate = 0.02
|
||||||
safety_risk = 0.18
|
safety_risk = 0.18
|
||||||
comfort_distance = 22.0
|
comfort_distance = 22.0
|
||||||
discovery_priority = 0.25
|
discovery_priority = 0.25
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ static var _all: Array[ResourceNode] = []
|
|||||||
@export var resource_id: StringName = SimulationIds.RESOURCE_FOOD
|
@export var resource_id: StringName = SimulationIds.RESOURCE_FOOD
|
||||||
@export var initial_amount: float = 10.0
|
@export var initial_amount: float = 10.0
|
||||||
@export var yield_per_action: float = 2.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 initial_enabled: bool = true
|
||||||
@export var can_npcs_use: bool = true
|
@export var can_npcs_use: bool = true
|
||||||
@export var can_player_use: bool = true
|
@export var can_player_use: bool = true
|
||||||
|
|||||||
Reference in New Issue
Block a user