feat: add player standing and quest generation from village needs
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
||||||
|
const SimulationEventRecorderScript := preload("res://simulation/events/SimulationEventRecorder.gd")
|
||||||
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
||||||
const AnimalCareSystemScript := preload("res://simulation/animals/animal_care_system.gd")
|
const AnimalCareSystemScript := preload("res://simulation/animals/animal_care_system.gd")
|
||||||
const NpcTickDebugLog := preload("res://simulation/debug/npc_tick_debug_log.gd")
|
const NpcTickDebugLog := preload("res://simulation/debug/npc_tick_debug_log.gd")
|
||||||
@@ -23,6 +24,10 @@ signal event_knowledge_forgotten(knower_id: int, event: EconomicEventRecord)
|
|||||||
signal opportunity_opened(opportunity: OpportunityStateRecord)
|
signal opportunity_opened(opportunity: OpportunityStateRecord)
|
||||||
signal opportunity_resolved(opportunity: OpportunityStateRecord, cause_event: EconomicEventRecord)
|
signal opportunity_resolved(opportunity: OpportunityStateRecord, cause_event: EconomicEventRecord)
|
||||||
signal opportunity_invalidated(opportunity: OpportunityStateRecord)
|
signal opportunity_invalidated(opportunity: OpportunityStateRecord)
|
||||||
|
signal player_quest_opened(quest: PlayerQuestRecord)
|
||||||
|
signal player_quest_completed(quest: PlayerQuestRecord)
|
||||||
|
signal player_quest_expired(quest: PlayerQuestRecord)
|
||||||
|
signal player_standing_changed(standing: PlayerStandingRecord)
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
|
|
||||||
@@ -39,11 +44,13 @@ var tick_count := 0
|
|||||||
var wander_random_sources := {}
|
var wander_random_sources := {}
|
||||||
var resource_states: Dictionary = {}
|
var resource_states: Dictionary = {}
|
||||||
var event_log := SimulationEventLogScript.new()
|
var event_log := SimulationEventLogScript.new()
|
||||||
|
var event_recorder := SimulationEventRecorderScript.new()
|
||||||
var economy := VillageEconomyScript.new()
|
var economy := VillageEconomyScript.new()
|
||||||
var animal_care := AnimalCareSystemScript.new()
|
var animal_care := AnimalCareSystemScript.new()
|
||||||
var relationship_system := RelationshipSystemScript.new()
|
var relationship_system := RelationshipSystemScript.new()
|
||||||
var event_knowledge_system := EventKnowledgeSystemScript.new()
|
var event_knowledge_system := EventKnowledgeSystemScript.new()
|
||||||
var opportunity_system := VillageOpportunitySystem.new()
|
var opportunity_system := VillageOpportunitySystem.new()
|
||||||
|
var player_quest_system := PlayerQuestSystem.new()
|
||||||
var storage_states: Dictionary:
|
var storage_states: Dictionary:
|
||||||
get:
|
get:
|
||||||
return economy.storage_states
|
return economy.storage_states
|
||||||
@@ -71,11 +78,13 @@ const NPC_NAMES := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("simulation_manager")
|
add_to_group("simulation_manager")
|
||||||
event_log.event_recorded.connect(_on_economic_event_recorded)
|
event_log.event_recorded.connect(_on_economic_event_recorded)
|
||||||
|
event_recorder.configure(event_log, Callable(self, "get_tick_count_for_recording"))
|
||||||
|
event_recorder.set_npcs(npcs)
|
||||||
economy.inventory_changed.connect(_on_economy_inventory_changed)
|
economy.inventory_changed.connect(_on_economy_inventory_changed)
|
||||||
economy.economic_event_requested.connect(_record_economic_event)
|
economy.economic_event_requested.connect(event_recorder.record_economic)
|
||||||
economy.narrative_event_requested.connect(record_narrative_event)
|
economy.narrative_event_requested.connect(event_recorder.record_narrative)
|
||||||
animal_care.economic_event_requested.connect(_record_economic_event_at)
|
animal_care.economic_event_requested.connect(event_recorder.record_economic_at)
|
||||||
animal_care.narrative_event_requested.connect(record_narrative_event)
|
animal_care.narrative_event_requested.connect(event_recorder.record_narrative)
|
||||||
action_selector.relationship_system = relationship_system
|
action_selector.relationship_system = relationship_system
|
||||||
var definition_errors := SimulationDefinitions.validate()
|
var definition_errors := SimulationDefinitions.validate()
|
||||||
if not definition_errors.is_empty():
|
if not definition_errors.is_empty():
|
||||||
@@ -175,6 +184,9 @@ func simulate_tick() -> void:
|
|||||||
)
|
)
|
||||||
if invalidated != null:
|
if invalidated != null:
|
||||||
opportunity_invalidated.emit(invalidated)
|
opportunity_invalidated.emit(invalidated)
|
||||||
|
var expired_quest := player_quest_system.on_opportunity_invalidated(invalidated, tick_count)
|
||||||
|
if expired_quest != null:
|
||||||
|
player_quest_expired.emit(expired_quest)
|
||||||
if tick_count % get_knowledge_review_interval() == 0:
|
if tick_count % get_knowledge_review_interval() == 0:
|
||||||
_maintain_event_knowledge(true)
|
_maintain_event_knowledge(true)
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
@@ -239,7 +251,7 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
|
|||||||
var display_name := (
|
var display_name := (
|
||||||
definition.display_name if definition != null else String(selection.action_id)
|
definition.display_name if definition != null else String(selection.action_id)
|
||||||
)
|
)
|
||||||
record_narrative_event(SimulationIds.EVENT_TASK_STARTED, npc.id, &"", display_name)
|
event_recorder.record_narrative(SimulationIds.EVENT_TASK_STARTED, npc.id, &"", display_name)
|
||||||
|
|
||||||
|
|
||||||
func _handle_npc_death(npc: SimNPC, previous_task: StringName, previous_target: StringName) -> void:
|
func _handle_npc_death(npc: SimNPC, previous_task: StringName, previous_target: StringName) -> void:
|
||||||
@@ -247,7 +259,7 @@ func _handle_npc_death(npc: SimNPC, previous_task: StringName, previous_target:
|
|||||||
release_npc_reservation(npc.id)
|
release_npc_reservation(npc.id)
|
||||||
npc_died.emit(npc)
|
npc_died.emit(npc)
|
||||||
npc_task_changed.emit(npc, previous_task, npc.current_task)
|
npc_task_changed.emit(npc, previous_task, npc.current_task)
|
||||||
record_narrative_event(SimulationIds.EVENT_NPC_DIED, npc.id)
|
event_recorder.record_narrative(SimulationIds.EVENT_NPC_DIED, npc.id)
|
||||||
_notify_mourning(npc)
|
_notify_mourning(npc)
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("[SimulationManager] NPC died: ", npc.npc_name)
|
print("[SimulationManager] NPC died: ", npc.npc_name)
|
||||||
@@ -293,7 +305,7 @@ func _apply_action_completion(
|
|||||||
SimulationIds.ACTION_SLEEP:
|
SimulationIds.ACTION_SLEEP:
|
||||||
npc.energy = minf(npc.energy + 40.0, 100.0)
|
npc.energy = minf(npc.energy + 40.0, 100.0)
|
||||||
npc.position = npc.home_position
|
npc.position = npc.home_position
|
||||||
record_narrative_event(SimulationIds.EVENT_NPC_SLEPT, npc.id)
|
event_recorder.record_narrative(SimulationIds.EVENT_NPC_SLEPT, npc.id)
|
||||||
SimulationIds.ACTION_FEED_ANIMAL:
|
SimulationIds.ACTION_FEED_ANIMAL:
|
||||||
pass
|
pass
|
||||||
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
||||||
@@ -320,7 +332,7 @@ func _complete_resource_gather(npc: SimNPC, completed_task: StringName) -> void:
|
|||||||
var resource_id := resource_state.get_resource_id()
|
var resource_id := resource_state.get_resource_id()
|
||||||
npc.add_inventory(resource_id, extracted)
|
npc.add_inventory(resource_id, extracted)
|
||||||
npc_inventory_changed.emit(npc, resource_id, npc.get_inventory_amount(resource_id))
|
npc_inventory_changed.emit(npc, resource_id, npc.get_inventory_amount(resource_id))
|
||||||
_record_economic_event(
|
event_recorder.record_economic(
|
||||||
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
||||||
npc.id,
|
npc.id,
|
||||||
resource_state.get_node_id(),
|
resource_state.get_node_id(),
|
||||||
@@ -329,7 +341,7 @@ func _complete_resource_gather(npc: SimNPC, completed_task: StringName) -> void:
|
|||||||
extracted
|
extracted
|
||||||
)
|
)
|
||||||
if resource_state.get_amount_remaining() <= 0.0:
|
if resource_state.get_amount_remaining() <= 0.0:
|
||||||
record_narrative_event(
|
event_recorder.record_narrative(
|
||||||
SimulationIds.EVENT_RESOURCE_DEPLETED, npc.id, resource_state.get_node_id()
|
SimulationIds.EVENT_RESOURCE_DEPLETED, npc.id, resource_state.get_node_id()
|
||||||
)
|
)
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
@@ -540,93 +552,8 @@ func _notify_mourning(dead_npc: SimNPC) -> void:
|
|||||||
print("[SimulationManager] ", mourner.npc_name, " is mourning ", dead_npc.npc_name)
|
print("[SimulationManager] ", mourner.npc_name, " is mourning ", dead_npc.npc_name)
|
||||||
|
|
||||||
|
|
||||||
func _record_economic_event(
|
func get_tick_count_for_recording() -> int:
|
||||||
event_type: StringName,
|
return tick_count
|
||||||
actor_id: int,
|
|
||||||
source_id: StringName,
|
|
||||||
destination_id: StringName,
|
|
||||||
item_id: StringName,
|
|
||||||
amount: float
|
|
||||||
) -> void:
|
|
||||||
_record_economic_event_at(
|
|
||||||
event_type,
|
|
||||||
actor_id,
|
|
||||||
source_id,
|
|
||||||
destination_id,
|
|
||||||
item_id,
|
|
||||||
amount,
|
|
||||||
_get_event_world_position(actor_id, source_id, destination_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _record_economic_event_at(
|
|
||||||
event_type: StringName,
|
|
||||||
actor_id: int,
|
|
||||||
source_id: StringName,
|
|
||||||
destination_id: StringName,
|
|
||||||
item_id: StringName,
|
|
||||||
amount: float,
|
|
||||||
world_position: Vector3
|
|
||||||
) -> void:
|
|
||||||
event_log.record_economic(
|
|
||||||
tick_count, event_type, actor_id, source_id, destination_id, item_id, amount, world_position
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func record_narrative_event(
|
|
||||||
event_type: StringName,
|
|
||||||
actor_id: int,
|
|
||||||
source_id: StringName = &"",
|
|
||||||
action_display: String = "",
|
|
||||||
action_id: StringName = &"",
|
|
||||||
item_id: StringName = &"",
|
|
||||||
required_amount: float = 0.0
|
|
||||||
) -> void:
|
|
||||||
_record_narrative_event_at(
|
|
||||||
event_type,
|
|
||||||
actor_id,
|
|
||||||
source_id,
|
|
||||||
action_display,
|
|
||||||
_get_event_world_position(actor_id, source_id, &""),
|
|
||||||
action_id,
|
|
||||||
item_id,
|
|
||||||
required_amount
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _record_narrative_event_at(
|
|
||||||
event_type: StringName,
|
|
||||||
actor_id: int,
|
|
||||||
source_id: StringName,
|
|
||||||
action_display: String,
|
|
||||||
world_position: Vector3,
|
|
||||||
action_id: StringName = &"",
|
|
||||||
item_id: StringName = &"",
|
|
||||||
required_amount: float = 0.0
|
|
||||||
) -> void:
|
|
||||||
event_log.record_narrative(
|
|
||||||
tick_count,
|
|
||||||
event_type,
|
|
||||||
actor_id,
|
|
||||||
source_id,
|
|
||||||
action_display,
|
|
||||||
world_position,
|
|
||||||
action_id,
|
|
||||||
item_id,
|
|
||||||
required_amount
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _get_event_world_position(
|
|
||||||
actor_id: int, source_id: StringName, destination_id: StringName
|
|
||||||
) -> Vector3:
|
|
||||||
for npc in npcs:
|
|
||||||
if npc.id != actor_id:
|
|
||||||
continue
|
|
||||||
if not npc.target_id.is_empty() and npc.target_id in [source_id, destination_id]:
|
|
||||||
return npc.travel_target_position
|
|
||||||
return npc.position
|
|
||||||
return Vector3.ZERO
|
|
||||||
|
|
||||||
|
|
||||||
func get_npc_events(npc_id: int, max_count: int = 8) -> Array[EconomicEventRecord]:
|
func get_npc_events(npc_id: int, max_count: int = 8) -> Array[EconomicEventRecord]:
|
||||||
@@ -729,8 +656,21 @@ func _update_opportunity_from_event(event: EconomicEventRecord) -> void:
|
|||||||
return
|
return
|
||||||
if changed.get_status() == OpportunityStateRecord.STATUS_OPEN:
|
if changed.get_status() == OpportunityStateRecord.STATUS_OPEN:
|
||||||
opportunity_opened.emit(changed)
|
opportunity_opened.emit(changed)
|
||||||
|
var quest := player_quest_system.consider_opportunity_opened(
|
||||||
|
changed, get_active_opportunity_player_response(), tick_count
|
||||||
|
)
|
||||||
|
if quest != null:
|
||||||
|
player_quest_opened.emit(quest)
|
||||||
return
|
return
|
||||||
opportunity_resolved.emit(changed, event)
|
opportunity_resolved.emit(changed, event)
|
||||||
|
var resolution := player_quest_system.on_opportunity_resolved(changed, event, tick_count)
|
||||||
|
if resolution.has("quest"):
|
||||||
|
var quest: PlayerQuestRecord = resolution["quest"]
|
||||||
|
if resolution["completed"]:
|
||||||
|
player_quest_completed.emit(quest)
|
||||||
|
player_standing_changed.emit(player_quest_system.standing)
|
||||||
|
else:
|
||||||
|
player_quest_expired.emit(quest)
|
||||||
_maintain_event_knowledge(false)
|
_maintain_event_knowledge(false)
|
||||||
|
|
||||||
|
|
||||||
@@ -816,6 +756,22 @@ func get_active_opportunity() -> OpportunityStateRecord:
|
|||||||
return opportunity_system.get_open_opportunity()
|
return opportunity_system.get_open_opportunity()
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_player_quest() -> PlayerQuestRecord:
|
||||||
|
return player_quest_system.get_active_quest()
|
||||||
|
|
||||||
|
|
||||||
|
func get_player_quests() -> Array[PlayerQuestRecord]:
|
||||||
|
return player_quest_system.get_all_sorted()
|
||||||
|
|
||||||
|
|
||||||
|
func get_latest_player_quest_for_requester(requester_npc_id: int) -> PlayerQuestRecord:
|
||||||
|
return player_quest_system.get_latest_for_requester(requester_npc_id)
|
||||||
|
|
||||||
|
|
||||||
|
func get_player_standing() -> PlayerStandingRecord:
|
||||||
|
return player_quest_system.standing
|
||||||
|
|
||||||
|
|
||||||
func get_active_opportunity_helper() -> OpportunityHelperResult:
|
func get_active_opportunity_helper() -> OpportunityHelperResult:
|
||||||
var active := get_active_opportunity()
|
var active := get_active_opportunity()
|
||||||
if active == null:
|
if active == null:
|
||||||
@@ -1015,7 +971,7 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
|||||||
if node.interaction_point != null
|
if node.interaction_point != null
|
||||||
else node.global_position
|
else node.global_position
|
||||||
)
|
)
|
||||||
_record_economic_event_at(
|
event_recorder.record_economic_at(
|
||||||
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
||||||
-1,
|
-1,
|
||||||
resource_state.get_node_id(),
|
resource_state.get_node_id(),
|
||||||
@@ -1033,7 +989,7 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
|||||||
event_position
|
event_position
|
||||||
)
|
)
|
||||||
if resource_state.get_amount_remaining() <= 0.0:
|
if resource_state.get_amount_remaining() <= 0.0:
|
||||||
_record_narrative_event_at(
|
event_recorder.record_narrative_at(
|
||||||
SimulationIds.EVENT_RESOURCE_DEPLETED,
|
SimulationIds.EVENT_RESOURCE_DEPLETED,
|
||||||
-1,
|
-1,
|
||||||
resource_state.get_node_id(),
|
resource_state.get_node_id(),
|
||||||
@@ -1101,6 +1057,7 @@ func create_state_record() -> SimulationStateRecord:
|
|||||||
"wander_random_streams": wander_streams,
|
"wander_random_streams": wander_streams,
|
||||||
"next_event_id": next_event_id,
|
"next_event_id": next_event_id,
|
||||||
"next_opportunity_id": opportunity_system.next_opportunity_id,
|
"next_opportunity_id": opportunity_system.next_opportunity_id,
|
||||||
|
"next_quest_id": player_quest_system.next_quest_id,
|
||||||
"cycle_duration_seconds": clock.cycle_duration_seconds
|
"cycle_duration_seconds": clock.cycle_duration_seconds
|
||||||
}
|
}
|
||||||
record.village = VillageStateRecord.capture(village)
|
record.village = VillageStateRecord.capture(village)
|
||||||
@@ -1125,6 +1082,9 @@ func create_state_record() -> SimulationStateRecord:
|
|||||||
record.event_knowledge.append(known_event)
|
record.event_knowledge.append(known_event)
|
||||||
for opportunity in opportunity_system.get_all_sorted():
|
for opportunity in opportunity_system.get_all_sorted():
|
||||||
record.opportunities.append(opportunity)
|
record.opportunities.append(opportunity)
|
||||||
|
record.player_standing = player_quest_system.standing
|
||||||
|
for quest in player_quest_system.get_all_sorted():
|
||||||
|
record.player_quests.append(quest)
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
@@ -1162,6 +1122,9 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
|||||||
relationship_system.restore(record.relationships)
|
relationship_system.restore(record.relationships)
|
||||||
event_knowledge_system.restore(record.event_knowledge)
|
event_knowledge_system.restore(record.event_knowledge)
|
||||||
opportunity_system.restore(record.opportunities, int(record.simulation["next_opportunity_id"]))
|
opportunity_system.restore(record.opportunities, int(record.simulation["next_opportunity_id"]))
|
||||||
|
player_quest_system.restore(
|
||||||
|
record.player_quests, int(record.simulation.get("next_quest_id", 0)), record.player_standing
|
||||||
|
)
|
||||||
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
|
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
|
||||||
wander_random_sources.clear()
|
wander_random_sources.clear()
|
||||||
var wander_streams: Array = record.simulation["wander_random_streams"]
|
var wander_streams: Array = record.simulation["wander_random_streams"]
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
class_name SimulationEventRecorder
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
||||||
|
|
||||||
|
var event_log: RefCounted
|
||||||
|
var npcs: Array[SimNPC] = []
|
||||||
|
var tick_provider: Callable
|
||||||
|
|
||||||
|
|
||||||
|
func configure(log: RefCounted, tick_source: Callable) -> void:
|
||||||
|
event_log = log
|
||||||
|
tick_provider = tick_source
|
||||||
|
|
||||||
|
|
||||||
|
func set_npcs(npc_list: Array[SimNPC]) -> void:
|
||||||
|
npcs = npc_list
|
||||||
|
|
||||||
|
|
||||||
|
func record_economic(
|
||||||
|
event_type: StringName,
|
||||||
|
actor_id: int,
|
||||||
|
source_id: StringName,
|
||||||
|
destination_id: StringName,
|
||||||
|
item_id: StringName,
|
||||||
|
amount: float
|
||||||
|
) -> void:
|
||||||
|
record_economic_at(
|
||||||
|
event_type,
|
||||||
|
actor_id,
|
||||||
|
source_id,
|
||||||
|
destination_id,
|
||||||
|
item_id,
|
||||||
|
amount,
|
||||||
|
get_event_world_position(actor_id, source_id, destination_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func record_economic_at(
|
||||||
|
event_type: StringName,
|
||||||
|
actor_id: int,
|
||||||
|
source_id: StringName,
|
||||||
|
destination_id: StringName,
|
||||||
|
item_id: StringName,
|
||||||
|
amount: float,
|
||||||
|
world_position: Vector3
|
||||||
|
) -> void:
|
||||||
|
event_log.record_economic(
|
||||||
|
tick_provider.call(),
|
||||||
|
event_type,
|
||||||
|
actor_id,
|
||||||
|
source_id,
|
||||||
|
destination_id,
|
||||||
|
item_id,
|
||||||
|
amount,
|
||||||
|
world_position
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func record_narrative(
|
||||||
|
event_type: StringName,
|
||||||
|
actor_id: int,
|
||||||
|
source_id: StringName = &"",
|
||||||
|
action_display: String = "",
|
||||||
|
action_id: StringName = &"",
|
||||||
|
item_id: StringName = &"",
|
||||||
|
required_amount: float = 0.0
|
||||||
|
) -> void:
|
||||||
|
record_narrative_at(
|
||||||
|
event_type,
|
||||||
|
actor_id,
|
||||||
|
source_id,
|
||||||
|
action_display,
|
||||||
|
get_event_world_position(actor_id, source_id, &""),
|
||||||
|
action_id,
|
||||||
|
item_id,
|
||||||
|
required_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func record_narrative_at(
|
||||||
|
event_type: StringName,
|
||||||
|
actor_id: int,
|
||||||
|
source_id: StringName,
|
||||||
|
action_display: String,
|
||||||
|
world_position: Vector3,
|
||||||
|
action_id: StringName = &"",
|
||||||
|
item_id: StringName = &"",
|
||||||
|
required_amount: float = 0.0
|
||||||
|
) -> void:
|
||||||
|
event_log.record_narrative(
|
||||||
|
tick_provider.call(),
|
||||||
|
event_type,
|
||||||
|
actor_id,
|
||||||
|
source_id,
|
||||||
|
action_display,
|
||||||
|
world_position,
|
||||||
|
action_id,
|
||||||
|
item_id,
|
||||||
|
required_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_event_world_position(
|
||||||
|
actor_id: int, source_id: StringName, destination_id: StringName
|
||||||
|
) -> Vector3:
|
||||||
|
for npc in npcs:
|
||||||
|
if npc.id != actor_id:
|
||||||
|
continue
|
||||||
|
if not npc.target_id.is_empty() and npc.target_id in [source_id, destination_id]:
|
||||||
|
return npc.travel_target_position
|
||||||
|
return npc.position
|
||||||
|
return Vector3.ZERO
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://yl07qwlrpxjs
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
class_name PlayerQuestSystem
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const PANTRY_STANDING_REWARD := 8.0
|
||||||
|
const WOOD_STANDING_REWARD := 8.0
|
||||||
|
|
||||||
|
var quests: Array[PlayerQuestRecord] = []
|
||||||
|
var next_quest_id := 0
|
||||||
|
var standing := PlayerStandingRecord.create()
|
||||||
|
|
||||||
|
|
||||||
|
func consider_opportunity_opened(
|
||||||
|
opportunity: OpportunityStateRecord,
|
||||||
|
player_response: OpportunityPlayerResponseResult,
|
||||||
|
current_tick: int
|
||||||
|
) -> PlayerQuestRecord:
|
||||||
|
if (
|
||||||
|
opportunity == null
|
||||||
|
or not opportunity.is_open()
|
||||||
|
or player_response == null
|
||||||
|
or current_tick < 0
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
if _find_open_quest_for_opportunity(opportunity.get_opportunity_id()) != null:
|
||||||
|
return null
|
||||||
|
var reward := _standing_reward_for(opportunity)
|
||||||
|
if reward <= 0.0:
|
||||||
|
return null
|
||||||
|
var quest := PlayerQuestRecord.create(
|
||||||
|
next_quest_id,
|
||||||
|
opportunity.get_opportunity_id(),
|
||||||
|
opportunity.get_interested_npc_id(),
|
||||||
|
opportunity.get_opportunity_type(),
|
||||||
|
opportunity.get_resource_id(),
|
||||||
|
opportunity.get_target_id(),
|
||||||
|
opportunity.get_target_amount(),
|
||||||
|
current_tick,
|
||||||
|
reward
|
||||||
|
)
|
||||||
|
next_quest_id += 1
|
||||||
|
quests.append(quest)
|
||||||
|
return quest
|
||||||
|
|
||||||
|
|
||||||
|
func on_opportunity_resolved(
|
||||||
|
opportunity: OpportunityStateRecord, resolution_event: EconomicEventRecord, current_tick: int
|
||||||
|
) -> Dictionary:
|
||||||
|
var quest := _find_open_quest_for_opportunity(opportunity.get_opportunity_id())
|
||||||
|
if quest == null or resolution_event == null:
|
||||||
|
return {}
|
||||||
|
var event_id := int(resolution_event.data["event_id"])
|
||||||
|
var actor_id := int(resolution_event.data["actor_id"])
|
||||||
|
if actor_id < 0:
|
||||||
|
if not quest.complete(event_id, current_tick):
|
||||||
|
return {}
|
||||||
|
standing.grant_standing(quest.get_standing_reward(), quest.get_requester_npc_id())
|
||||||
|
return {"quest": quest, "completed": true}
|
||||||
|
if quest.expire(current_tick):
|
||||||
|
return {"quest": quest, "completed": false}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func on_opportunity_invalidated(
|
||||||
|
opportunity: OpportunityStateRecord, current_tick: int
|
||||||
|
) -> PlayerQuestRecord:
|
||||||
|
var quest := _find_open_quest_for_opportunity(opportunity.get_opportunity_id())
|
||||||
|
if quest == null:
|
||||||
|
return null
|
||||||
|
if quest.expire(current_tick):
|
||||||
|
return quest
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_quest() -> PlayerQuestRecord:
|
||||||
|
for quest in quests:
|
||||||
|
if quest.is_open():
|
||||||
|
return quest
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_all_sorted() -> Array[PlayerQuestRecord]:
|
||||||
|
var sorted := quests.duplicate()
|
||||||
|
sorted.sort_custom(_sort_by_id)
|
||||||
|
return sorted
|
||||||
|
|
||||||
|
|
||||||
|
func get_latest_for_requester(requester_npc_id: int) -> PlayerQuestRecord:
|
||||||
|
for index in range(quests.size() - 1, -1, -1):
|
||||||
|
var quest := quests[index]
|
||||||
|
if quest.get_requester_npc_id() == requester_npc_id:
|
||||||
|
return quest
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func restore(
|
||||||
|
records: Array[PlayerQuestRecord], restored_next_id: int, standing_record: PlayerStandingRecord
|
||||||
|
) -> void:
|
||||||
|
quests = records.duplicate()
|
||||||
|
quests.sort_custom(_sort_by_id)
|
||||||
|
next_quest_id = maxi(restored_next_id, 0)
|
||||||
|
standing = standing_record if standing_record != null else PlayerStandingRecord.create()
|
||||||
|
|
||||||
|
|
||||||
|
func _find_open_quest_for_opportunity(opportunity_id: int) -> PlayerQuestRecord:
|
||||||
|
for quest in quests:
|
||||||
|
if quest.is_open() and quest.get_opportunity_id() == opportunity_id:
|
||||||
|
return quest
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _standing_reward_for(opportunity: OpportunityStateRecord) -> float:
|
||||||
|
match opportunity.get_opportunity_type():
|
||||||
|
SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY:
|
||||||
|
return PANTRY_STANDING_REWARD
|
||||||
|
SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD:
|
||||||
|
return WOOD_STANDING_REWARD
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
static func _sort_by_id(first: PlayerQuestRecord, second: PlayerQuestRecord) -> bool:
|
||||||
|
return first.get_quest_id() < second.get_quest_id()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cojertujqx8ts
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
class_name PlayerQuestRecord
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const SCHEMA_VERSION := 1
|
||||||
|
const STATUS_OPEN := &"open"
|
||||||
|
const STATUS_COMPLETED := &"completed"
|
||||||
|
const STATUS_EXPIRED := &"expired"
|
||||||
|
|
||||||
|
var data: Dictionary
|
||||||
|
|
||||||
|
|
||||||
|
func _init(record_data: Dictionary = {}) -> void:
|
||||||
|
data = record_data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
static func create(
|
||||||
|
quest_id: int,
|
||||||
|
opportunity_id: int,
|
||||||
|
requester_npc_id: int,
|
||||||
|
quest_type: StringName,
|
||||||
|
resource_id: StringName,
|
||||||
|
target_id: StringName,
|
||||||
|
target_amount: float,
|
||||||
|
created_tick: int,
|
||||||
|
standing_reward: float
|
||||||
|
) -> PlayerQuestRecord:
|
||||||
|
return (
|
||||||
|
PlayerQuestRecord
|
||||||
|
. new(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"quest_id": quest_id,
|
||||||
|
"opportunity_id": opportunity_id,
|
||||||
|
"requester_npc_id": requester_npc_id,
|
||||||
|
"quest_type": String(quest_type),
|
||||||
|
"resource_id": String(resource_id),
|
||||||
|
"target_id": String(target_id),
|
||||||
|
"target_amount": target_amount,
|
||||||
|
"status": String(STATUS_OPEN),
|
||||||
|
"created_tick": created_tick,
|
||||||
|
"standing_reward": standing_reward,
|
||||||
|
"resolution_event_id": -1,
|
||||||
|
"resolved_tick": -1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func from_dictionary(record_data: Dictionary) -> PlayerQuestRecord:
|
||||||
|
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||||
|
return null
|
||||||
|
if not (
|
||||||
|
record_data
|
||||||
|
. has_all(
|
||||||
|
[
|
||||||
|
"quest_id",
|
||||||
|
"opportunity_id",
|
||||||
|
"requester_npc_id",
|
||||||
|
"quest_type",
|
||||||
|
"resource_id",
|
||||||
|
"target_id",
|
||||||
|
"target_amount",
|
||||||
|
"status",
|
||||||
|
"created_tick",
|
||||||
|
"standing_reward",
|
||||||
|
"resolution_event_id",
|
||||||
|
"resolved_tick",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
var quest_id := int(record_data["quest_id"])
|
||||||
|
var opportunity_id := int(record_data["opportunity_id"])
|
||||||
|
var requester_npc_id := int(record_data["requester_npc_id"])
|
||||||
|
var quest_type := StringName(record_data["quest_type"])
|
||||||
|
var resource_id := StringName(record_data["resource_id"])
|
||||||
|
var target_id := StringName(record_data["target_id"])
|
||||||
|
var target_amount := float(record_data["target_amount"])
|
||||||
|
var status := StringName(record_data["status"])
|
||||||
|
var created_tick := int(record_data["created_tick"])
|
||||||
|
var standing_reward := float(record_data["standing_reward"])
|
||||||
|
var resolution_event_id := int(record_data["resolution_event_id"])
|
||||||
|
var resolved_tick := int(record_data["resolved_tick"])
|
||||||
|
if (
|
||||||
|
quest_id < 0
|
||||||
|
or opportunity_id < 0
|
||||||
|
or requester_npc_id < 0
|
||||||
|
or quest_type.is_empty()
|
||||||
|
or resource_id.is_empty()
|
||||||
|
or target_id.is_empty()
|
||||||
|
or not is_finite(target_amount)
|
||||||
|
or target_amount <= 0.0
|
||||||
|
or created_tick < 0
|
||||||
|
or not is_finite(standing_reward)
|
||||||
|
or standing_reward <= 0.0
|
||||||
|
or resolution_event_id < -1
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
match status:
|
||||||
|
STATUS_OPEN:
|
||||||
|
if resolution_event_id != -1 or resolved_tick != -1:
|
||||||
|
return null
|
||||||
|
STATUS_COMPLETED:
|
||||||
|
if resolution_event_id < 0 or resolved_tick < created_tick:
|
||||||
|
return null
|
||||||
|
STATUS_EXPIRED:
|
||||||
|
if resolution_event_id != -1 or resolved_tick < created_tick:
|
||||||
|
return null
|
||||||
|
_:
|
||||||
|
return null
|
||||||
|
var record := create(
|
||||||
|
quest_id,
|
||||||
|
opportunity_id,
|
||||||
|
requester_npc_id,
|
||||||
|
quest_type,
|
||||||
|
resource_id,
|
||||||
|
target_id,
|
||||||
|
target_amount,
|
||||||
|
created_tick,
|
||||||
|
standing_reward
|
||||||
|
)
|
||||||
|
if status == STATUS_COMPLETED:
|
||||||
|
record.complete(resolution_event_id, resolved_tick)
|
||||||
|
elif status == STATUS_EXPIRED:
|
||||||
|
record.expire(resolved_tick)
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
func get_quest_id() -> int:
|
||||||
|
return int(data["quest_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_opportunity_id() -> int:
|
||||||
|
return int(data["opportunity_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_requester_npc_id() -> int:
|
||||||
|
return int(data["requester_npc_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_quest_type() -> StringName:
|
||||||
|
return StringName(data["quest_type"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_resource_id() -> StringName:
|
||||||
|
return StringName(data["resource_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_target_id() -> StringName:
|
||||||
|
return StringName(data["target_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_target_amount() -> float:
|
||||||
|
return float(data["target_amount"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_status() -> StringName:
|
||||||
|
return StringName(data["status"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_created_tick() -> int:
|
||||||
|
return int(data["created_tick"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_standing_reward() -> float:
|
||||||
|
return float(data["standing_reward"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_resolution_event_id() -> int:
|
||||||
|
return int(data["resolution_event_id"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_resolved_tick() -> int:
|
||||||
|
return int(data["resolved_tick"])
|
||||||
|
|
||||||
|
|
||||||
|
func is_open() -> bool:
|
||||||
|
return get_status() == STATUS_OPEN
|
||||||
|
|
||||||
|
|
||||||
|
func complete(resolution_event_id: int, resolved_tick: int) -> bool:
|
||||||
|
if not is_open() or resolution_event_id < 0 or resolved_tick < get_created_tick():
|
||||||
|
return false
|
||||||
|
data["status"] = String(STATUS_COMPLETED)
|
||||||
|
data["resolution_event_id"] = resolution_event_id
|
||||||
|
data["resolved_tick"] = resolved_tick
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func expire(resolved_tick: int) -> bool:
|
||||||
|
if not is_open() or resolved_tick < get_created_tick():
|
||||||
|
return false
|
||||||
|
data["status"] = String(STATUS_EXPIRED)
|
||||||
|
data["resolved_tick"] = resolved_tick
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func to_dictionary() -> Dictionary:
|
||||||
|
return data.duplicate(true)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://rix0e3rygdoo
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
class_name PlayerStandingRecord
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const SCHEMA_VERSION := 1
|
||||||
|
const MAX_STANDING := 100.0
|
||||||
|
const TIER_STRANGER := 0
|
||||||
|
const TIER_KNOWN_HAND := 1
|
||||||
|
const TIER_TRUSTED := 2
|
||||||
|
const TIER_VILLAGE_STEWARD := 3
|
||||||
|
const TIER_VOICE_OF_JAJCE := 4
|
||||||
|
|
||||||
|
const TIER_THRESHOLDS := {
|
||||||
|
TIER_STRANGER: 0.0,
|
||||||
|
TIER_KNOWN_HAND: 15.0,
|
||||||
|
TIER_TRUSTED: 35.0,
|
||||||
|
TIER_VILLAGE_STEWARD: 60.0,
|
||||||
|
TIER_VOICE_OF_JAJCE: 85.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIER_NAMES := {
|
||||||
|
TIER_STRANGER: "Stranger",
|
||||||
|
TIER_KNOWN_HAND: "Known Hand",
|
||||||
|
TIER_TRUSTED: "Trusted",
|
||||||
|
TIER_VILLAGE_STEWARD: "Village Steward",
|
||||||
|
TIER_VOICE_OF_JAJCE: "Voice of Jajce",
|
||||||
|
}
|
||||||
|
|
||||||
|
var data: Dictionary
|
||||||
|
|
||||||
|
|
||||||
|
func _init(record_data: Dictionary = {}) -> void:
|
||||||
|
data = record_data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
static func create() -> PlayerStandingRecord:
|
||||||
|
return (
|
||||||
|
PlayerStandingRecord
|
||||||
|
. new(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"standing": 0.0,
|
||||||
|
"resolved_needs": 0,
|
||||||
|
"gratitude": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func from_dictionary(record_data: Dictionary) -> PlayerStandingRecord:
|
||||||
|
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||||
|
return null
|
||||||
|
if not record_data.has_all(["standing", "resolved_needs", "gratitude"]):
|
||||||
|
return null
|
||||||
|
var standing := float(record_data["standing"])
|
||||||
|
var resolved_needs := int(record_data["resolved_needs"])
|
||||||
|
var gratitude = record_data["gratitude"]
|
||||||
|
if (
|
||||||
|
not is_finite(standing)
|
||||||
|
or standing < 0.0
|
||||||
|
or standing > MAX_STANDING
|
||||||
|
or resolved_needs < 0
|
||||||
|
or not gratitude is Dictionary
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
var normalized_gratitude := {}
|
||||||
|
for raw_npc_id in gratitude:
|
||||||
|
var npc_id := int(raw_npc_id)
|
||||||
|
var value := float(gratitude[raw_npc_id])
|
||||||
|
if npc_id < 0 or not is_finite(value) or value < 0.0 or value > 1.0:
|
||||||
|
return null
|
||||||
|
normalized_gratitude[npc_id] = value
|
||||||
|
return (
|
||||||
|
PlayerStandingRecord
|
||||||
|
. new(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"standing": standing,
|
||||||
|
"resolved_needs": resolved_needs,
|
||||||
|
"gratitude": normalized_gratitude,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func grant_standing(amount: float, requester_npc_id: int) -> void:
|
||||||
|
if not is_finite(amount) or amount <= 0.0 or requester_npc_id < 0:
|
||||||
|
return
|
||||||
|
data["standing"] = minf(get_standing() + amount, MAX_STANDING)
|
||||||
|
data["resolved_needs"] = int(data["resolved_needs"]) + 1
|
||||||
|
var gratitude := get_gratitude(requester_npc_id)
|
||||||
|
data["gratitude"][requester_npc_id] = minf(gratitude + 0.1, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func get_standing() -> float:
|
||||||
|
return float(data["standing"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_resolved_needs() -> int:
|
||||||
|
return int(data["resolved_needs"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_gratitude(npc_id: int) -> float:
|
||||||
|
return float(data["gratitude"].get(npc_id, 0.0))
|
||||||
|
|
||||||
|
|
||||||
|
func get_tier() -> int:
|
||||||
|
var current := TIER_STRANGER
|
||||||
|
for tier in [TIER_VOICE_OF_JAJCE, TIER_VILLAGE_STEWARD, TIER_TRUSTED, TIER_KNOWN_HAND]:
|
||||||
|
if get_standing() >= float(TIER_THRESHOLDS[tier]):
|
||||||
|
current = tier
|
||||||
|
break
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
func get_tier_name() -> String:
|
||||||
|
return String(TIER_NAMES[get_tier()])
|
||||||
|
|
||||||
|
|
||||||
|
func to_dictionary() -> Dictionary:
|
||||||
|
return data.duplicate(true)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://v827brnqruus
|
||||||
@@ -2,7 +2,7 @@ class_name SimulationStateRecord
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_NAME := "the_steward.simulation"
|
const SCHEMA_NAME := "the_steward.simulation"
|
||||||
const SCHEMA_VERSION := 11
|
const SCHEMA_VERSION := 12
|
||||||
const LEGACY_SCHEMA_VERSION := 1
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||||
@@ -13,6 +13,7 @@ const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8
|
|||||||
const ANIMAL_LEGACY_SCHEMA_VERSION := 9
|
const ANIMAL_LEGACY_SCHEMA_VERSION := 9
|
||||||
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
|
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
|
||||||
const PREVIOUS_SCHEMA_VERSION := 7
|
const PREVIOUS_SCHEMA_VERSION := 7
|
||||||
|
const PLAYER_LEGACY_SCHEMA_VERSION := 11
|
||||||
|
|
||||||
var simulation: Dictionary
|
var simulation: Dictionary
|
||||||
var village: VillageStateRecord
|
var village: VillageStateRecord
|
||||||
@@ -24,6 +25,8 @@ var economic_events: Array[EconomicEventRecord] = []
|
|||||||
var relationships: Array[RelationshipStateRecord] = []
|
var relationships: Array[RelationshipStateRecord] = []
|
||||||
var event_knowledge: Array[KnownEventStateRecord] = []
|
var event_knowledge: Array[KnownEventStateRecord] = []
|
||||||
var opportunities: Array[OpportunityStateRecord] = []
|
var opportunities: Array[OpportunityStateRecord] = []
|
||||||
|
var player_standing: PlayerStandingRecord
|
||||||
|
var player_quests: Array[PlayerQuestRecord] = []
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
@@ -52,6 +55,9 @@ func to_dictionary() -> Dictionary:
|
|||||||
var opportunity_data: Array[Dictionary] = []
|
var opportunity_data: Array[Dictionary] = []
|
||||||
for opportunity_record in opportunities:
|
for opportunity_record in opportunities:
|
||||||
opportunity_data.append(opportunity_record.to_dictionary())
|
opportunity_data.append(opportunity_record.to_dictionary())
|
||||||
|
var player_quest_data: Array[Dictionary] = []
|
||||||
|
for player_quest_record in player_quests:
|
||||||
|
player_quest_data.append(player_quest_record.to_dictionary())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"schema": SCHEMA_NAME,
|
"schema": SCHEMA_NAME,
|
||||||
@@ -65,7 +71,9 @@ func to_dictionary() -> Dictionary:
|
|||||||
"economic_events": event_data,
|
"economic_events": event_data,
|
||||||
"relationships": relationship_data,
|
"relationships": relationship_data,
|
||||||
"event_knowledge": knowledge_data,
|
"event_knowledge": knowledge_data,
|
||||||
"opportunities": opportunity_data
|
"opportunities": opportunity_data,
|
||||||
|
"player_standing": player_standing.to_dictionary(),
|
||||||
|
"player_quests": player_quest_data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -97,6 +105,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
||||||
ANIMAL_LEGACY_SCHEMA_VERSION,
|
ANIMAL_LEGACY_SCHEMA_VERSION,
|
||||||
ROUTINE_LEGACY_SCHEMA_VERSION,
|
ROUTINE_LEGACY_SCHEMA_VERSION,
|
||||||
|
PLAYER_LEGACY_SCHEMA_VERSION,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
record_data = _migrate_legacy(record_data, version)
|
record_data = _migrate_legacy(record_data, version)
|
||||||
@@ -116,6 +125,8 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
"relationships",
|
"relationships",
|
||||||
"event_knowledge",
|
"event_knowledge",
|
||||||
"opportunities",
|
"opportunities",
|
||||||
|
"player_standing",
|
||||||
|
"player_quests",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
@@ -418,6 +429,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
record.relationships.append(relationship_record)
|
record.relationships.append(relationship_record)
|
||||||
|
|
||||||
var opportunity_ids := {}
|
var opportunity_ids := {}
|
||||||
|
var opportunity_records_by_id := {}
|
||||||
var trigger_event_ids := {}
|
var trigger_event_ids := {}
|
||||||
var resolution_event_ids := {}
|
var resolution_event_ids := {}
|
||||||
var has_open_opportunity := false
|
var has_open_opportunity := false
|
||||||
@@ -452,12 +464,58 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
opportunity_ids[opportunity_id] = true
|
opportunity_ids[opportunity_id] = true
|
||||||
|
opportunity_records_by_id[opportunity_id] = opportunity_record
|
||||||
trigger_event_ids[trigger_event_id] = true
|
trigger_event_ids[trigger_event_id] = true
|
||||||
highest_opportunity_id = maxi(highest_opportunity_id, opportunity_id)
|
highest_opportunity_id = maxi(highest_opportunity_id, opportunity_id)
|
||||||
record.opportunities.append(opportunity_record)
|
record.opportunities.append(opportunity_record)
|
||||||
if int(record.simulation["next_opportunity_id"]) <= highest_opportunity_id:
|
if int(record.simulation["next_opportunity_id"]) <= highest_opportunity_id:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
var standing_data = record_data["player_standing"]
|
||||||
|
if not standing_data is Dictionary:
|
||||||
|
return null
|
||||||
|
var standing_record := PlayerStandingRecord.from_dictionary(standing_data)
|
||||||
|
if standing_record == null:
|
||||||
|
return null
|
||||||
|
record.player_standing = standing_record
|
||||||
|
|
||||||
|
var player_quest_data = record_data["player_quests"]
|
||||||
|
if not player_quest_data is Array:
|
||||||
|
return null
|
||||||
|
var quest_ids := {}
|
||||||
|
var quest_opportunity_ids := {}
|
||||||
|
var highest_quest_id := -1
|
||||||
|
for item in player_quest_data:
|
||||||
|
if not item is Dictionary:
|
||||||
|
return null
|
||||||
|
var quest_record := PlayerQuestRecord.from_dictionary(item)
|
||||||
|
if quest_record == null:
|
||||||
|
return null
|
||||||
|
var quest_id := quest_record.get_quest_id()
|
||||||
|
var quest_opportunity_id := quest_record.get_opportunity_id()
|
||||||
|
var requester_id := quest_record.get_requester_npc_id()
|
||||||
|
if (
|
||||||
|
quest_ids.has(quest_id)
|
||||||
|
or quest_opportunity_ids.has(quest_opportunity_id)
|
||||||
|
or not npc_ids.has(requester_id)
|
||||||
|
or not opportunity_ids.has(quest_opportunity_id)
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
if (
|
||||||
|
quest_record.get_status() == PlayerQuestRecord.STATUS_OPEN
|
||||||
|
and not opportunity_records_by_id.has(quest_opportunity_id)
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
if (
|
||||||
|
quest_record.get_resolution_event_id() >= 0
|
||||||
|
and not event_ids.has(quest_record.get_resolution_event_id())
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
quest_ids[quest_id] = true
|
||||||
|
quest_opportunity_ids[quest_opportunity_id] = true
|
||||||
|
highest_quest_id = maxi(highest_quest_id, quest_id)
|
||||||
|
record.player_quests.append(quest_record)
|
||||||
|
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
@@ -772,6 +830,12 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
|||||||
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
|
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
|
||||||
opportunity_simulation_data["next_opportunity_id"] = 0
|
opportunity_simulation_data["next_opportunity_id"] = 0
|
||||||
migrated["simulation"] = opportunity_simulation_data
|
migrated["simulation"] = opportunity_simulation_data
|
||||||
|
if version <= PLAYER_LEGACY_SCHEMA_VERSION:
|
||||||
|
migrated["player_standing"] = PlayerStandingRecord.create().to_dictionary()
|
||||||
|
migrated["player_quests"] = []
|
||||||
|
var quest_simulation_data: Dictionary = migrated.get("simulation", {})
|
||||||
|
quest_simulation_data["next_quest_id"] = 0
|
||||||
|
migrated["simulation"] = quest_simulation_data
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var opened_quest_ids: Array[int] = []
|
||||||
|
var completed_quest_ids: Array[int] = []
|
||||||
|
var expired_quest_ids: Array[int] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var manager := _create_manager()
|
||||||
|
manager.player_quest_opened.connect(_on_quest_opened)
|
||||||
|
manager.player_quest_completed.connect(_on_quest_completed)
|
||||||
|
manager.player_quest_expired.connect(_on_quest_expired)
|
||||||
|
var berry := _register_berry(manager, &"quest_berry_bush")
|
||||||
|
var actor: SimNPC = manager.npcs[0]
|
||||||
|
var interested: SimNPC = manager.npcs[2]
|
||||||
|
_set_pantry_amount(manager, 1.0)
|
||||||
|
for npc in manager.npcs:
|
||||||
|
npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0)
|
||||||
|
actor.position = Vector3.ZERO
|
||||||
|
actor.hunger = 60.0
|
||||||
|
interested.hunger = 90.0
|
||||||
|
manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
|
_prepare_shared_patrol(actor, interested, Vector3(20.0, 0.0, 0.0))
|
||||||
|
_check(
|
||||||
|
manager.try_communicate_at_shared_activity(actor.id, interested.id),
|
||||||
|
"The quest proof should open the same known pantry shortage"
|
||||||
|
)
|
||||||
|
_check(manager.get_active_opportunity() != null, "The quest proof needs an active opportunity")
|
||||||
|
var quest: PlayerQuestRecord = manager.get_active_player_quest()
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
quest != null
|
||||||
|
and quest.get_requester_npc_id() == interested.id
|
||||||
|
and quest.get_quest_type() == SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY
|
||||||
|
and quest.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||||
|
and quest.get_target_id() == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||||
|
and is_equal_approx(quest.get_target_amount(), 1.0)
|
||||||
|
and quest.get_standing_reward() > 0.0
|
||||||
|
and quest.is_open()
|
||||||
|
and opened_quest_ids == [quest.get_quest_id()]
|
||||||
|
),
|
||||||
|
"A player-actionable shortage should generate one quest for its worried villager",
|
||||||
|
)
|
||||||
|
if quest == null:
|
||||||
|
manager.free()
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
|
||||||
|
var no_helper_route: OpportunityPlayerResponseResult = (
|
||||||
|
manager.get_active_opportunity_player_response()
|
||||||
|
)
|
||||||
|
var standing_before: PlayerStandingRecord = manager.get_player_standing()
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
no_helper_route != null
|
||||||
|
and is_equal_approx(standing_before.get_standing(), 0.0)
|
||||||
|
and standing_before.get_resolved_needs() == 0
|
||||||
|
and is_equal_approx(standing_before.get_gratitude(interested.id), 0.0)
|
||||||
|
),
|
||||||
|
"Standing should start at zero with no gratitude before any player help",
|
||||||
|
)
|
||||||
|
|
||||||
|
var checksum_before_resolve: String = manager.get_state_checksum()
|
||||||
|
var serialized_before: String = manager.serialize_state()
|
||||||
|
var berry_harvested: float = manager.harvest_resource_node(berry)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(berry_harvested, 1.0),
|
||||||
|
"Completing the quest should go through the ordinary player harvest command"
|
||||||
|
)
|
||||||
|
var standing_after: PlayerStandingRecord = manager.get_player_standing()
|
||||||
|
var completed: PlayerQuestRecord = manager.get_active_player_quest()
|
||||||
|
var latest: PlayerQuestRecord = manager.get_latest_player_quest_for_requester(interested.id)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
completed == null
|
||||||
|
and latest != null
|
||||||
|
and latest.get_status() == PlayerQuestRecord.STATUS_COMPLETED
|
||||||
|
and completed_quest_ids == [latest.get_quest_id()]
|
||||||
|
and is_equal_approx(standing_after.get_standing(), quest.get_standing_reward())
|
||||||
|
and standing_after.get_resolved_needs() == 1
|
||||||
|
and is_equal_approx(standing_after.get_gratitude(interested.id), 0.1)
|
||||||
|
and manager.get_state_checksum() != checksum_before_resolve
|
||||||
|
),
|
||||||
|
"A real player supply should complete the quest, grant standing, and raise gratitude",
|
||||||
|
)
|
||||||
|
|
||||||
|
var saved_json: String = serialized_before
|
||||||
|
var pre_completion_state: String = manager.serialize_state()
|
||||||
|
var restored := _create_manager(902)
|
||||||
|
_check(
|
||||||
|
restored.restore_state_from_json(pre_completion_state),
|
||||||
|
"The completed quest and standing should restore through the current schema"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
restored.get_state_checksum() == manager.get_state_checksum()
|
||||||
|
and restored.get_active_player_quest() == null
|
||||||
|
and restored.get_latest_player_quest_for_requester(interested.id) != null
|
||||||
|
and is_equal_approx(
|
||||||
|
restored.get_player_standing().get_standing(), quest.get_standing_reward()
|
||||||
|
)
|
||||||
|
and is_equal_approx(restored.get_player_standing().get_gratitude(interested.id), 0.1)
|
||||||
|
),
|
||||||
|
"Restore should re-derive completed quest and standing without reopening the need",
|
||||||
|
)
|
||||||
|
restored.free()
|
||||||
|
|
||||||
|
var invalidated := _create_manager(905)
|
||||||
|
invalidated.player_quest_expired.connect(_on_quest_expired)
|
||||||
|
var stale_tree := _register_tree(invalidated, &"quest_stale_tree")
|
||||||
|
var stale_setup := _open_missing_wood_need(invalidated)
|
||||||
|
_check(
|
||||||
|
invalidated.get_active_player_quest() != null, "The invalidation branch needs an open quest"
|
||||||
|
)
|
||||||
|
var stale_quest: PlayerQuestRecord = invalidated.get_active_player_quest()
|
||||||
|
var stale_interested_id: int = stale_setup["interested"].id
|
||||||
|
var review_interval: int = invalidated.get_knowledge_review_interval()
|
||||||
|
invalidated.tick_count = stale_quest.get_created_tick() + review_interval
|
||||||
|
invalidated.call("_maintain_event_knowledge", true)
|
||||||
|
invalidated.simulate_tick()
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
expired_quest_ids.has(stale_quest.get_quest_id())
|
||||||
|
and invalidated.get_active_player_quest() == null
|
||||||
|
and is_equal_approx(
|
||||||
|
invalidated.get_player_standing().get_gratitude(stale_interested_id), 0.0
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"A stale need should expire its quest without granting standing",
|
||||||
|
)
|
||||||
|
berry.queue_free()
|
||||||
|
stale_tree.queue_free()
|
||||||
|
manager.free()
|
||||||
|
invalidated.free()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _register_berry(manager: Node, node_id: StringName) -> ResourceNode:
|
||||||
|
var berry := ResourceNode.new()
|
||||||
|
berry.name = "QuestBerryBush"
|
||||||
|
berry.node_id = node_id
|
||||||
|
berry.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
berry.resource_id = SimulationIds.RESOURCE_FOOD
|
||||||
|
berry.initial_amount = 1.0
|
||||||
|
berry.yield_per_action = 1.0
|
||||||
|
berry.debug_label_enabled = false
|
||||||
|
var interaction_point := Marker3D.new()
|
||||||
|
interaction_point.name = "InteractionPoint"
|
||||||
|
berry.add_child(interaction_point)
|
||||||
|
root.add_child(berry)
|
||||||
|
_check(
|
||||||
|
manager.register_resource_node(berry),
|
||||||
|
"The quest proof should bind a real finite food ResourceNode"
|
||||||
|
)
|
||||||
|
return berry
|
||||||
|
|
||||||
|
|
||||||
|
func _register_tree(manager: Node, node_id: StringName) -> ResourceNode:
|
||||||
|
var tree := ResourceNode.new()
|
||||||
|
tree.name = "QuestTree"
|
||||||
|
tree.node_id = node_id
|
||||||
|
tree.action_id = SimulationIds.ACTION_GATHER_WOOD
|
||||||
|
tree.resource_id = SimulationIds.RESOURCE_WOOD
|
||||||
|
tree.initial_amount = 1.0
|
||||||
|
tree.yield_per_action = 1.0
|
||||||
|
tree.debug_label_enabled = false
|
||||||
|
var interaction_point := Marker3D.new()
|
||||||
|
interaction_point.name = "InteractionPoint"
|
||||||
|
tree.add_child(interaction_point)
|
||||||
|
root.add_child(tree)
|
||||||
|
_check(
|
||||||
|
manager.register_resource_node(tree),
|
||||||
|
"The invalidation proof should bind a real finite wood ResourceNode"
|
||||||
|
)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
func _open_missing_wood_need(manager: Node) -> Dictionary:
|
||||||
|
var actor: SimNPC = manager.npcs[0]
|
||||||
|
var witness: SimNPC = manager.npcs[1]
|
||||||
|
actor.position = Vector3.ZERO
|
||||||
|
witness.position = Vector3(4.0, 0.0, 0.0)
|
||||||
|
var woodpile: StorageStateRecord = manager.get_woodpile()
|
||||||
|
woodpile.withdraw(SimulationIds.RESOURCE_WOOD, woodpile.get_amount(SimulationIds.RESOURCE_WOOD))
|
||||||
|
woodpile.deposit(SimulationIds.RESOURCE_WOOD, 0.5)
|
||||||
|
manager.economy.sync_resource(SimulationIds.RESOURCE_WOOD)
|
||||||
|
actor.set_task(SimulationIds.ACTION_PATROL, 1.0)
|
||||||
|
actor.start_working()
|
||||||
|
manager.simulate_tick()
|
||||||
|
var opportunity: OpportunityStateRecord = manager.get_active_opportunity()
|
||||||
|
_check(
|
||||||
|
opportunity != null, "The invalidation branch needs the performer's known blocked-work need"
|
||||||
|
)
|
||||||
|
return {"actor": actor, "interested": actor}
|
||||||
|
|
||||||
|
|
||||||
|
func _create_manager(seed_value: int = 901) -> 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 _open_via_communication(manager: Node) -> Dictionary:
|
||||||
|
var actor: SimNPC = manager.npcs[0]
|
||||||
|
var interested: SimNPC = manager.npcs[2]
|
||||||
|
_set_pantry_amount(manager, 1.0)
|
||||||
|
for npc in manager.npcs:
|
||||||
|
npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0)
|
||||||
|
actor.position = Vector3.ZERO
|
||||||
|
actor.hunger = 60.0
|
||||||
|
interested.hunger = 90.0
|
||||||
|
manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
|
_prepare_shared_patrol(actor, interested, Vector3(20.0, 0.0, 0.0))
|
||||||
|
_check(
|
||||||
|
manager.try_communicate_at_shared_activity(actor.id, interested.id),
|
||||||
|
"The invalidation branch should open from the same one-hop known shortage"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
manager.get_active_opportunity() != null,
|
||||||
|
"The invalidation branch needs an active opportunity"
|
||||||
|
)
|
||||||
|
return {"actor": actor, "interested": interested}
|
||||||
|
|
||||||
|
|
||||||
|
func _prepare_shared_patrol(first: SimNPC, second: SimNPC, position: Vector3) -> void:
|
||||||
|
for npc in [first, second]:
|
||||||
|
npc.set_task(SimulationIds.ACTION_PATROL)
|
||||||
|
npc.target_id = &"guard_post"
|
||||||
|
npc.start_working()
|
||||||
|
first.position = position
|
||||||
|
second.position = position + Vector3(1.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_pantry_amount(manager: Node, amount: float) -> void:
|
||||||
|
var pantry: StorageStateRecord = manager.get_pantry()
|
||||||
|
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
||||||
|
pantry.deposit(SimulationIds.RESOURCE_FOOD, amount)
|
||||||
|
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_quest_opened(quest: PlayerQuestRecord) -> void:
|
||||||
|
opened_quest_ids.append(quest.get_quest_id())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_quest_completed(quest: PlayerQuestRecord) -> void:
|
||||||
|
completed_quest_ids.append(quest.get_quest_id())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_quest_expired(quest: PlayerQuestRecord) -> void:
|
||||||
|
expired_quest_ids.append(quest.get_quest_id())
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Player quest generation and standing passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://s2vxgyidroc0
|
||||||
Reference in New Issue
Block a user