feat: add knowledge-gated pantry opportunities
This commit is contained in:
@@ -4,6 +4,9 @@ const SimulationEventLogScript := preload("res://simulation/events/SimulationEve
|
||||
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
||||
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
||||
const FoodShortageOpportunitySystemScript := preload(
|
||||
"res://simulation/opportunities/FoodShortageOpportunitySystem.gd"
|
||||
)
|
||||
|
||||
signal npc_task_changed(npc: SimNPC, old_task: StringName, new_task: StringName)
|
||||
signal village_changed(village: SimVillage)
|
||||
@@ -18,6 +21,8 @@ signal relationship_changed(relationship: RelationshipStateRecord, cause_event:
|
||||
signal event_knowledge_changed(knower_id: int, event: EconomicEventRecord)
|
||||
signal event_knowledge_transferred(speaker_id: int, listener_id: int, event: EconomicEventRecord)
|
||||
signal event_knowledge_forgotten(knower_id: int, event: EconomicEventRecord)
|
||||
signal opportunity_opened(opportunity: OpportunityStateRecord)
|
||||
signal opportunity_resolved(opportunity: OpportunityStateRecord, cause_event: EconomicEventRecord)
|
||||
|
||||
var village := SimVillage.new()
|
||||
|
||||
@@ -37,6 +42,7 @@ var event_log := SimulationEventLogScript.new()
|
||||
var economy := VillageEconomyScript.new()
|
||||
var relationship_system := RelationshipSystemScript.new()
|
||||
var event_knowledge_system := EventKnowledgeSystemScript.new()
|
||||
var opportunity_system := FoodShortageOpportunitySystemScript.new()
|
||||
var storage_states: Dictionary:
|
||||
get:
|
||||
return economy.storage_states
|
||||
@@ -595,6 +601,7 @@ func _on_economic_event_recorded(event: EconomicEventRecord) -> void:
|
||||
)
|
||||
for learned_record in learned_records:
|
||||
_apply_new_event_knowledge(learned_record, event)
|
||||
_update_opportunity_from_event(event)
|
||||
economic_event_recorded.emit(event)
|
||||
|
||||
|
||||
@@ -617,6 +624,7 @@ func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bo
|
||||
continue
|
||||
_apply_new_event_knowledge(learned_record, event)
|
||||
event_knowledge_transferred.emit(speaker.id, listener.id, event)
|
||||
_update_opportunity_from_event(event)
|
||||
return true
|
||||
return false
|
||||
|
||||
@@ -663,6 +671,19 @@ func _apply_new_event_knowledge(
|
||||
_maintain_event_knowledge(false)
|
||||
|
||||
|
||||
func _update_opportunity_from_event(event: EconomicEventRecord) -> void:
|
||||
var changed: OpportunityStateRecord = opportunity_system.consider_event(
|
||||
event, tick_count, get_pantry(), npcs, event_knowledge_system, resource_states
|
||||
)
|
||||
if changed == null:
|
||||
return
|
||||
if changed.get_status() == OpportunityStateRecord.STATUS_OPEN:
|
||||
opportunity_opened.emit(changed)
|
||||
return
|
||||
opportunity_resolved.emit(changed, event)
|
||||
_maintain_event_knowledge(false)
|
||||
|
||||
|
||||
func _maintain_event_knowledge(expire_by_age: bool) -> void:
|
||||
var forgotten := event_knowledge_system.maintain_retention(
|
||||
tick_count, get_knowledge_review_interval(), _get_lasting_knowledge_records(), expire_by_age
|
||||
@@ -688,6 +709,18 @@ func _get_lasting_knowledge_records() -> Array[KnownEventStateRecord]:
|
||||
continue
|
||||
seen[key] = true
|
||||
lasting.append(record)
|
||||
var open_opportunity: OpportunityStateRecord = opportunity_system.get_open_opportunity()
|
||||
if open_opportunity != null:
|
||||
var opportunity_record := event_knowledge_system.get_record(
|
||||
open_opportunity.get_interested_npc_id(), open_opportunity.get_trigger_event_id()
|
||||
)
|
||||
if opportunity_record != null:
|
||||
var opportunity_key := (
|
||||
"%d:%d" % [opportunity_record.get_knower_id(), opportunity_record.get_event_id()]
|
||||
)
|
||||
if not seen.has(opportunity_key):
|
||||
seen[opportunity_key] = true
|
||||
lasting.append(opportunity_record)
|
||||
return lasting
|
||||
|
||||
|
||||
@@ -742,6 +775,29 @@ func get_retained_memory_events(npc_id: int, max_count: int = 4) -> Array[Econom
|
||||
return retained
|
||||
|
||||
|
||||
func get_active_opportunity() -> OpportunityStateRecord:
|
||||
return opportunity_system.get_open_opportunity()
|
||||
|
||||
|
||||
func get_latest_opportunity_for_npc(npc_id: int) -> OpportunityStateRecord:
|
||||
return opportunity_system.get_latest_for_npc(npc_id)
|
||||
|
||||
|
||||
func get_opportunity_trigger_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
||||
if opportunity == null:
|
||||
return null
|
||||
return event_log.get_by_id(opportunity.get_trigger_event_id())
|
||||
|
||||
|
||||
func get_opportunity_resolution_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
||||
if (
|
||||
opportunity == null
|
||||
or opportunity.get_resolution_event_id() == OpportunityStateRecord.NO_EVENT_ID
|
||||
):
|
||||
return null
|
||||
return event_log.get_by_id(opportunity.get_resolution_event_id())
|
||||
|
||||
|
||||
func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
||||
return event_knowledge_system.knows_event(npc_id, event_id)
|
||||
|
||||
@@ -1032,6 +1088,7 @@ func create_state_record() -> SimulationStateRecord:
|
||||
"clock_elapsed_ticks": clock.elapsed_ticks,
|
||||
"wander_random_streams": wander_streams,
|
||||
"next_event_id": next_event_id,
|
||||
"next_opportunity_id": opportunity_system.next_opportunity_id,
|
||||
"cycle_duration_seconds": clock.cycle_duration_seconds
|
||||
}
|
||||
record.village = VillageStateRecord.capture(village)
|
||||
@@ -1054,6 +1111,8 @@ func create_state_record() -> SimulationStateRecord:
|
||||
record.relationships.append(relationship)
|
||||
for known_event in event_knowledge_system.get_all_sorted():
|
||||
record.event_knowledge.append(known_event)
|
||||
for opportunity in opportunity_system.get_all_sorted():
|
||||
record.opportunities.append(opportunity)
|
||||
return record
|
||||
|
||||
|
||||
@@ -1091,6 +1150,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
npcs.append(npc_record.restore(debug_logs))
|
||||
relationship_system.restore(record.relationships)
|
||||
event_knowledge_system.restore(record.event_knowledge)
|
||||
opportunity_system.restore(record.opportunities, int(record.simulation["next_opportunity_id"]))
|
||||
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
|
||||
|
||||
wander_random_sources.clear()
|
||||
|
||||
@@ -41,6 +41,10 @@ const EVENT_TASK_STARTED := &"task_started"
|
||||
const EVENT_TASK_BLOCKED := &"task_blocked"
|
||||
const EVENT_RESOURCE_DEPLETED := &"resource_depleted"
|
||||
|
||||
const OPPORTUNITY_RESTOCK_EMPTY_PANTRY := &"restock_empty_pantry"
|
||||
const OPPORTUNITY_STATUS_OPEN := &"open"
|
||||
const OPPORTUNITY_STATUS_RESOLVED := &"resolved"
|
||||
|
||||
const KNOWLEDGE_ACQUISITION_PERFORMED := &"performed"
|
||||
const KNOWLEDGE_ACQUISITION_WITNESSED := &"witnessed"
|
||||
const KNOWLEDGE_ACQUISITION_COMMUNICATED := &"communicated"
|
||||
|
||||
@@ -9,7 +9,7 @@ var known_events: Dictionary = {}
|
||||
|
||||
func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[KnownEventStateRecord]:
|
||||
var learned: Array[KnownEventStateRecord] = []
|
||||
if not _is_witnessable(event):
|
||||
if not is_knowable_event(event):
|
||||
return learned
|
||||
var actor := _find_npc(int(event.data["actor_id"]), npcs)
|
||||
if actor == null:
|
||||
@@ -44,7 +44,7 @@ func communicate_event(
|
||||
event == null
|
||||
or speaker_id == listener_id
|
||||
or listener_id == int(event.data["actor_id"])
|
||||
or not _is_witnessable(event)
|
||||
or not is_knowable_event(event)
|
||||
):
|
||||
return null
|
||||
var event_id := int(event.data["event_id"])
|
||||
@@ -221,11 +221,21 @@ func _remember(
|
||||
return record
|
||||
|
||||
|
||||
func _is_witnessable(event: EconomicEventRecord) -> bool:
|
||||
static func is_knowable_event(event: EconomicEventRecord) -> bool:
|
||||
if event == null or float(event.data["amount"]) <= 0.0:
|
||||
return false
|
||||
var event_type := StringName(event.data["event_type"])
|
||||
var item_id := StringName(event.data["item_id"])
|
||||
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
|
||||
return item_id == SimulationIds.RESOURCE_FOOD
|
||||
if event_type != SimulationIds.EVENT_STORAGE_WITHDRAWN:
|
||||
return false
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
return (
|
||||
StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||
and float(event.data["amount"]) > 0.0
|
||||
actor_id >= 0
|
||||
and item_id == SimulationIds.RESOURCE_FOOD
|
||||
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and (StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id))
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
class_name FoodShortageOpportunitySystem
|
||||
extends RefCounted
|
||||
|
||||
const CARE_HUNGER_THRESHOLD := 80.0
|
||||
|
||||
var opportunities: Array[OpportunityStateRecord] = []
|
||||
var next_opportunity_id := 0
|
||||
|
||||
|
||||
func consider_event(
|
||||
event: EconomicEventRecord,
|
||||
current_tick: int,
|
||||
pantry: StorageStateRecord,
|
||||
npcs: Array[SimNPC],
|
||||
knowledge_system: EventKnowledgeSystem,
|
||||
resource_states: Dictionary
|
||||
) -> OpportunityStateRecord:
|
||||
if event == null or pantry == null or current_tick < 0:
|
||||
return null
|
||||
var event_tick := int(event.data["tick"])
|
||||
if event_tick < 0 or event_tick > current_tick:
|
||||
return null
|
||||
var resolved := _try_resolve(event, pantry, npcs, resource_states)
|
||||
if resolved != null:
|
||||
return resolved
|
||||
return _try_open(event, current_tick, pantry, npcs, knowledge_system)
|
||||
|
||||
|
||||
func restore(records: Array[OpportunityStateRecord], restored_next_id: int) -> void:
|
||||
opportunities = records.duplicate()
|
||||
opportunities.sort_custom(_sort_by_id)
|
||||
next_opportunity_id = maxi(restored_next_id, 0)
|
||||
|
||||
|
||||
func get_all_sorted() -> Array[OpportunityStateRecord]:
|
||||
var sorted := opportunities.duplicate()
|
||||
sorted.sort_custom(_sort_by_id)
|
||||
return sorted
|
||||
|
||||
|
||||
func get_open_sorted() -> Array[OpportunityStateRecord]:
|
||||
var open_records: Array[OpportunityStateRecord] = []
|
||||
for opportunity in opportunities:
|
||||
if opportunity.is_open():
|
||||
open_records.append(opportunity)
|
||||
open_records.sort_custom(_sort_by_id)
|
||||
return open_records
|
||||
|
||||
|
||||
func get_open_opportunity() -> OpportunityStateRecord:
|
||||
var open_records := get_open_sorted()
|
||||
return open_records[0] if not open_records.is_empty() else null
|
||||
|
||||
|
||||
func get_latest() -> OpportunityStateRecord:
|
||||
var sorted := get_all_sorted()
|
||||
return sorted[-1] if not sorted.is_empty() else null
|
||||
|
||||
|
||||
func get_latest_for_npc(npc_id: int) -> OpportunityStateRecord:
|
||||
for index in range(opportunities.size() - 1, -1, -1):
|
||||
var opportunity := opportunities[index]
|
||||
if opportunity.get_interested_npc_id() == npc_id:
|
||||
return opportunity
|
||||
return null
|
||||
|
||||
|
||||
func _try_open(
|
||||
event: EconomicEventRecord,
|
||||
current_tick: int,
|
||||
pantry: StorageStateRecord,
|
||||
npcs: Array[SimNPC],
|
||||
knowledge_system: EventKnowledgeSystem
|
||||
) -> OpportunityStateRecord:
|
||||
if knowledge_system == null or get_open_opportunity() != null:
|
||||
return null
|
||||
if pantry.get_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
|
||||
return null
|
||||
if not _is_food_withdrawal(event):
|
||||
return null
|
||||
var interested_npc := _select_interested_npc(
|
||||
int(event.data["event_id"]), npcs, knowledge_system
|
||||
)
|
||||
if interested_npc == null:
|
||||
return null
|
||||
var opportunity := OpportunityStateRecord.create(
|
||||
next_opportunity_id, current_tick, int(event.data["event_id"]), interested_npc.id
|
||||
)
|
||||
next_opportunity_id += 1
|
||||
opportunities.append(opportunity)
|
||||
return opportunity
|
||||
|
||||
|
||||
func _try_resolve(
|
||||
event: EconomicEventRecord,
|
||||
pantry: StorageStateRecord,
|
||||
npcs: Array[SimNPC],
|
||||
resource_states: Dictionary
|
||||
) -> OpportunityStateRecord:
|
||||
var opportunity := get_open_opportunity()
|
||||
if opportunity == null:
|
||||
return null
|
||||
if pantry.get_amount(SimulationIds.RESOURCE_FOOD) < opportunity.get_target_amount():
|
||||
return null
|
||||
if not _is_valid_supply(event, npcs, resource_states):
|
||||
return null
|
||||
if int(event.data["tick"]) < opportunity.get_created_tick():
|
||||
return null
|
||||
if int(event.data["event_id"]) <= opportunity.get_trigger_event_id():
|
||||
return null
|
||||
if not opportunity.resolve(int(event.data["event_id"]), int(event.data["tick"])):
|
||||
return null
|
||||
return opportunity
|
||||
|
||||
|
||||
func _select_interested_npc(
|
||||
event_id: int, npcs: Array[SimNPC], knowledge_system: EventKnowledgeSystem
|
||||
) -> SimNPC:
|
||||
var candidates: Array[SimNPC] = []
|
||||
for npc in npcs:
|
||||
if npc.is_dead or npc.hunger < CARE_HUNGER_THRESHOLD:
|
||||
continue
|
||||
if not knowledge_system.knows_event(npc.id, event_id):
|
||||
continue
|
||||
candidates.append(npc)
|
||||
candidates.sort_custom(_sort_by_care)
|
||||
return candidates[0] if not candidates.is_empty() else null
|
||||
|
||||
|
||||
static func _is_food_withdrawal(event: EconomicEventRecord) -> bool:
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
return (
|
||||
StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_WITHDRAWN
|
||||
and actor_id >= 0
|
||||
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id)
|
||||
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||
and float(event.data["amount"]) > 0.0
|
||||
)
|
||||
|
||||
|
||||
static func _is_valid_supply(
|
||||
event: EconomicEventRecord, npcs: Array[SimNPC], resource_states: Dictionary
|
||||
) -> bool:
|
||||
if (
|
||||
StringName(event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
|
||||
or StringName(event.data["destination_id"]) != SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
or float(event.data["amount"]) <= 0.0
|
||||
):
|
||||
return false
|
||||
var event_type := StringName(event.data["event_type"])
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
|
||||
return (
|
||||
_find_npc(actor_id, npcs) != null
|
||||
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id)
|
||||
)
|
||||
if event_type != SimulationIds.EVENT_RESOURCE_EXTRACTED or actor_id != -1:
|
||||
return false
|
||||
var resource_state := (
|
||||
resource_states.get(StringName(event.data["source_id"])) as ResourceStateRecord
|
||||
)
|
||||
return (
|
||||
resource_state != null
|
||||
and resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||
and resource_state.can_player_use_resource()
|
||||
)
|
||||
|
||||
|
||||
static func _sort_by_care(first: SimNPC, second: SimNPC) -> bool:
|
||||
if first.hunger != second.hunger:
|
||||
return first.hunger > second.hunger
|
||||
return first.id < second.id
|
||||
|
||||
|
||||
static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
return npc
|
||||
return null
|
||||
|
||||
|
||||
static func _sort_by_id(first: OpportunityStateRecord, second: OpportunityStateRecord) -> bool:
|
||||
return first.get_opportunity_id() < second.get_opportunity_id()
|
||||
@@ -0,0 +1 @@
|
||||
uid://r6gpw0hx8oe2
|
||||
@@ -0,0 +1,167 @@
|
||||
class_name OpportunityStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const NO_EVENT_ID := -1
|
||||
const DEFAULT_TARGET_AMOUNT := 1.0
|
||||
const STATUS_OPEN := SimulationIds.OPPORTUNITY_STATUS_OPEN
|
||||
const STATUS_RESOLVED := SimulationIds.OPPORTUNITY_STATUS_RESOLVED
|
||||
|
||||
var data: Dictionary
|
||||
|
||||
|
||||
func _init(record_data: Dictionary = {}) -> void:
|
||||
data = record_data.duplicate(true)
|
||||
|
||||
|
||||
static func create(
|
||||
opportunity_id: int,
|
||||
created_tick: int,
|
||||
trigger_event_id: int,
|
||||
interested_npc_id: int,
|
||||
target_amount: float = DEFAULT_TARGET_AMOUNT
|
||||
) -> OpportunityStateRecord:
|
||||
return (
|
||||
OpportunityStateRecord
|
||||
. new(
|
||||
{
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"opportunity_id": opportunity_id,
|
||||
"opportunity_type": String(SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY),
|
||||
"status": String(STATUS_OPEN),
|
||||
"created_tick": created_tick,
|
||||
"trigger_event_id": trigger_event_id,
|
||||
"interested_npc_id": interested_npc_id,
|
||||
"target_id": String(SimulationIds.STORAGE_VILLAGE_PANTRY),
|
||||
"resource_id": String(SimulationIds.RESOURCE_FOOD),
|
||||
"target_amount": target_amount,
|
||||
"resolution_event_id": NO_EVENT_ID,
|
||||
"resolved_tick": -1,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> OpportunityStateRecord:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if not (
|
||||
record_data
|
||||
. has_all(
|
||||
[
|
||||
"opportunity_id",
|
||||
"opportunity_type",
|
||||
"status",
|
||||
"created_tick",
|
||||
"trigger_event_id",
|
||||
"interested_npc_id",
|
||||
"target_id",
|
||||
"resource_id",
|
||||
"target_amount",
|
||||
"resolution_event_id",
|
||||
"resolved_tick",
|
||||
]
|
||||
)
|
||||
):
|
||||
return null
|
||||
var opportunity_id := int(record_data["opportunity_id"])
|
||||
var opportunity_type := StringName(record_data["opportunity_type"])
|
||||
var status := StringName(record_data["status"])
|
||||
var created_tick := int(record_data["created_tick"])
|
||||
var trigger_event_id := int(record_data["trigger_event_id"])
|
||||
var interested_npc_id := int(record_data["interested_npc_id"])
|
||||
var target_id := StringName(record_data["target_id"])
|
||||
var resource_id := StringName(record_data["resource_id"])
|
||||
var target_amount := float(record_data["target_amount"])
|
||||
var resolution_event_id := int(record_data["resolution_event_id"])
|
||||
var resolved_tick := int(record_data["resolved_tick"])
|
||||
if opportunity_id < 0 or created_tick < 0 or trigger_event_id < 0 or interested_npc_id < 0:
|
||||
return null
|
||||
if opportunity_type != SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY:
|
||||
return null
|
||||
if target_id != SimulationIds.STORAGE_VILLAGE_PANTRY:
|
||||
return null
|
||||
if resource_id != SimulationIds.RESOURCE_FOOD:
|
||||
return null
|
||||
if not is_finite(target_amount) or not is_equal_approx(target_amount, DEFAULT_TARGET_AMOUNT):
|
||||
return null
|
||||
match status:
|
||||
STATUS_OPEN:
|
||||
if resolution_event_id != NO_EVENT_ID or resolved_tick != -1:
|
||||
return null
|
||||
STATUS_RESOLVED:
|
||||
if resolution_event_id <= trigger_event_id or resolved_tick < created_tick:
|
||||
return null
|
||||
_:
|
||||
return null
|
||||
var record := create(
|
||||
opportunity_id, created_tick, trigger_event_id, interested_npc_id, target_amount
|
||||
)
|
||||
if status == STATUS_RESOLVED:
|
||||
record.resolve(resolution_event_id, resolved_tick)
|
||||
return record
|
||||
|
||||
|
||||
func resolve(resolution_event_id: int, resolved_tick: int) -> bool:
|
||||
if (
|
||||
not is_open()
|
||||
or resolution_event_id <= get_trigger_event_id()
|
||||
or resolved_tick < get_created_tick()
|
||||
):
|
||||
return false
|
||||
data["status"] = String(STATUS_RESOLVED)
|
||||
data["resolution_event_id"] = resolution_event_id
|
||||
data["resolved_tick"] = resolved_tick
|
||||
return true
|
||||
|
||||
|
||||
func get_opportunity_id() -> int:
|
||||
return int(data["opportunity_id"])
|
||||
|
||||
|
||||
func get_opportunity_type() -> StringName:
|
||||
return StringName(data["opportunity_type"])
|
||||
|
||||
|
||||
func get_status() -> StringName:
|
||||
return StringName(data["status"])
|
||||
|
||||
|
||||
func get_created_tick() -> int:
|
||||
return int(data["created_tick"])
|
||||
|
||||
|
||||
func get_trigger_event_id() -> int:
|
||||
return int(data["trigger_event_id"])
|
||||
|
||||
|
||||
func get_interested_npc_id() -> int:
|
||||
return int(data["interested_npc_id"])
|
||||
|
||||
|
||||
func get_target_id() -> StringName:
|
||||
return StringName(data["target_id"])
|
||||
|
||||
|
||||
func get_resource_id() -> StringName:
|
||||
return StringName(data["resource_id"])
|
||||
|
||||
|
||||
func get_target_amount() -> float:
|
||||
return float(data["target_amount"])
|
||||
|
||||
|
||||
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 to_dictionary() -> Dictionary:
|
||||
return data.duplicate(true)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ce2xl34unwkef
|
||||
@@ -2,13 +2,14 @@ class_name SimulationStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_NAME := "the_steward.simulation"
|
||||
const SCHEMA_VERSION := 7
|
||||
const SCHEMA_VERSION := 8
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
|
||||
const PROVENANCE_LEGACY_SCHEMA_VERSION := 5
|
||||
const PREVIOUS_SCHEMA_VERSION := 6
|
||||
const RETENTION_LEGACY_SCHEMA_VERSION := 6
|
||||
const PREVIOUS_SCHEMA_VERSION := 7
|
||||
|
||||
var simulation: Dictionary
|
||||
var village: VillageStateRecord
|
||||
@@ -18,6 +19,7 @@ var storages: Array[StorageStateRecord] = []
|
||||
var economic_events: Array[EconomicEventRecord] = []
|
||||
var relationships: Array[RelationshipStateRecord] = []
|
||||
var event_knowledge: Array[KnownEventStateRecord] = []
|
||||
var opportunities: Array[OpportunityStateRecord] = []
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
@@ -40,6 +42,9 @@ func to_dictionary() -> Dictionary:
|
||||
var knowledge_data: Array[Dictionary] = []
|
||||
for known_event_record in event_knowledge:
|
||||
knowledge_data.append(known_event_record.to_dictionary())
|
||||
var opportunity_data: Array[Dictionary] = []
|
||||
for opportunity_record in opportunities:
|
||||
opportunity_data.append(opportunity_record.to_dictionary())
|
||||
|
||||
return {
|
||||
"schema": SCHEMA_NAME,
|
||||
@@ -51,7 +56,8 @@ func to_dictionary() -> Dictionary:
|
||||
"storages": storage_data,
|
||||
"economic_events": event_data,
|
||||
"relationships": relationship_data,
|
||||
"event_knowledge": knowledge_data
|
||||
"event_knowledge": knowledge_data,
|
||||
"opportunities": opportunity_data
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +84,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
||||
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
||||
PROVENANCE_LEGACY_SCHEMA_VERSION,
|
||||
RETENTION_LEGACY_SCHEMA_VERSION,
|
||||
PREVIOUS_SCHEMA_VERSION,
|
||||
]
|
||||
):
|
||||
@@ -96,6 +103,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
"economic_events",
|
||||
"relationships",
|
||||
"event_knowledge",
|
||||
"opportunities",
|
||||
]
|
||||
)
|
||||
):
|
||||
@@ -104,22 +112,27 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
var simulation_data = record_data["simulation"]
|
||||
if not simulation_data is Dictionary:
|
||||
return null
|
||||
if not simulation_data.has_all(
|
||||
[
|
||||
"seed",
|
||||
"tick_interval",
|
||||
"tick_count",
|
||||
"clock_accumulator",
|
||||
"clock_elapsed_ticks",
|
||||
"wander_random_streams",
|
||||
"next_event_id"
|
||||
]
|
||||
if not (
|
||||
simulation_data
|
||||
. has_all(
|
||||
[
|
||||
"seed",
|
||||
"tick_interval",
|
||||
"tick_count",
|
||||
"clock_accumulator",
|
||||
"clock_elapsed_ticks",
|
||||
"wander_random_streams",
|
||||
"next_event_id",
|
||||
"next_opportunity_id",
|
||||
]
|
||||
)
|
||||
):
|
||||
return null
|
||||
var saved_tick_interval := float(simulation_data["tick_interval"])
|
||||
var saved_tick_count := int(simulation_data["tick_count"])
|
||||
var saved_clock_accumulator := float(simulation_data["clock_accumulator"])
|
||||
var saved_clock_elapsed_ticks := int(simulation_data["clock_elapsed_ticks"])
|
||||
var saved_next_opportunity_id := int(simulation_data["next_opportunity_id"])
|
||||
if (
|
||||
not is_finite(saved_tick_interval)
|
||||
or saved_tick_interval <= 0.0
|
||||
@@ -127,6 +140,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
or not is_finite(saved_clock_accumulator)
|
||||
or saved_clock_accumulator < 0.0
|
||||
or saved_clock_elapsed_ticks < 0
|
||||
or saved_next_opportunity_id < 0
|
||||
):
|
||||
return null
|
||||
if simulation_data.has("cycle_duration_seconds"):
|
||||
@@ -155,6 +169,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
var event_data = record_data["economic_events"]
|
||||
var relationship_data = record_data["relationships"]
|
||||
var knowledge_data = record_data["event_knowledge"]
|
||||
var opportunity_data = record_data["opportunities"]
|
||||
if (
|
||||
not npc_data is Array
|
||||
or not resource_data is Array
|
||||
@@ -162,6 +177,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
or not event_data is Array
|
||||
or not relationship_data is Array
|
||||
or not knowledge_data is Array
|
||||
or not opportunity_data is Array
|
||||
):
|
||||
return null
|
||||
|
||||
@@ -183,6 +199,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
record.npcs.append(npc_record)
|
||||
|
||||
var resource_ids := {}
|
||||
var resource_records_by_id := {}
|
||||
for item in resource_data:
|
||||
if not item is Dictionary:
|
||||
return null
|
||||
@@ -193,9 +210,11 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
if resource_ids.has(resource_id):
|
||||
return null
|
||||
resource_ids[resource_id] = true
|
||||
resource_records_by_id[resource_id] = resource_record
|
||||
record.resources.append(resource_record)
|
||||
|
||||
var storage_ids := {}
|
||||
var storage_records_by_id := {}
|
||||
for item in storage_data:
|
||||
if not item is Dictionary:
|
||||
return null
|
||||
@@ -206,6 +225,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
if storage_ids.has(storage_id):
|
||||
return null
|
||||
storage_ids[storage_id] = true
|
||||
storage_records_by_id[storage_id] = storage_record
|
||||
record.storages.append(storage_record)
|
||||
|
||||
var event_ids := {}
|
||||
@@ -228,6 +248,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
return null
|
||||
|
||||
var knowledge_keys := {}
|
||||
var knowledge_records_by_key := {}
|
||||
for item in knowledge_data:
|
||||
if not item is Dictionary:
|
||||
return null
|
||||
@@ -242,6 +263,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
if knowledge_keys.has(knowledge_key):
|
||||
return null
|
||||
knowledge_keys[knowledge_key] = true
|
||||
knowledge_records_by_key[knowledge_key] = known_event_record
|
||||
record.event_knowledge.append(known_event_record)
|
||||
for known_event_record in record.event_knowledge:
|
||||
if not _is_valid_knowledge_provenance(
|
||||
@@ -289,6 +311,46 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
relationship_keys[relationship_key] = true
|
||||
record.relationships.append(relationship_record)
|
||||
|
||||
var opportunity_ids := {}
|
||||
var trigger_event_ids := {}
|
||||
var resolution_event_ids := {}
|
||||
var has_open_opportunity := false
|
||||
var highest_opportunity_id := -1
|
||||
for item in opportunity_data:
|
||||
if not item is Dictionary:
|
||||
return null
|
||||
var opportunity_record := OpportunityStateRecord.from_dictionary(item)
|
||||
if opportunity_record == null:
|
||||
return null
|
||||
var opportunity_id := opportunity_record.get_opportunity_id()
|
||||
var trigger_event_id := opportunity_record.get_trigger_event_id()
|
||||
if opportunity_ids.has(opportunity_id) or trigger_event_ids.has(trigger_event_id):
|
||||
return null
|
||||
if opportunity_record.get_status() == OpportunityStateRecord.STATUS_OPEN:
|
||||
if has_open_opportunity:
|
||||
return null
|
||||
has_open_opportunity = true
|
||||
elif opportunity_record.get_resolution_event_id() != OpportunityStateRecord.NO_EVENT_ID:
|
||||
if resolution_event_ids.has(opportunity_record.get_resolution_event_id()):
|
||||
return null
|
||||
resolution_event_ids[opportunity_record.get_resolution_event_id()] = true
|
||||
if not _is_valid_opportunity(
|
||||
opportunity_record,
|
||||
npc_ids,
|
||||
storage_records_by_id,
|
||||
event_records_by_id,
|
||||
knowledge_records_by_key,
|
||||
resource_records_by_id,
|
||||
int(record.simulation["tick_count"])
|
||||
):
|
||||
return null
|
||||
opportunity_ids[opportunity_id] = true
|
||||
trigger_event_ids[trigger_event_id] = true
|
||||
highest_opportunity_id = maxi(highest_opportunity_id, opportunity_id)
|
||||
record.opportunities.append(opportunity_record)
|
||||
if int(record.simulation["next_opportunity_id"]) <= highest_opportunity_id:
|
||||
return null
|
||||
|
||||
return record
|
||||
|
||||
|
||||
@@ -310,11 +372,7 @@ static func _is_valid_knowledge_provenance(
|
||||
return false
|
||||
if method == SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
||||
return true
|
||||
if (
|
||||
StringName(event.data["event_type"]) != SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
or StringName(event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
|
||||
or float(event.data["amount"]) <= 0.0
|
||||
):
|
||||
if not EventKnowledgeSystem.is_knowable_event(event):
|
||||
return false
|
||||
match method:
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
||||
@@ -337,6 +395,117 @@ static func _is_valid_knowledge_provenance(
|
||||
return false
|
||||
|
||||
|
||||
static func _is_valid_opportunity(
|
||||
opportunity: OpportunityStateRecord,
|
||||
npc_ids: Dictionary,
|
||||
storage_records_by_id: Dictionary,
|
||||
event_records_by_id: Dictionary,
|
||||
knowledge_records_by_key: Dictionary,
|
||||
resource_records_by_id: Dictionary,
|
||||
current_tick: int
|
||||
) -> bool:
|
||||
if opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY:
|
||||
return false
|
||||
if (
|
||||
not npc_ids.has(opportunity.get_interested_npc_id())
|
||||
or opportunity.get_target_id() != SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
or not storage_records_by_id.has(opportunity.get_target_id())
|
||||
or opportunity.get_resource_id() != SimulationIds.RESOURCE_FOOD
|
||||
or not is_finite(opportunity.get_target_amount())
|
||||
or opportunity.get_target_amount() <= 0.0
|
||||
):
|
||||
return false
|
||||
var trigger_event := (
|
||||
event_records_by_id.get(opportunity.get_trigger_event_id()) as EconomicEventRecord
|
||||
)
|
||||
if trigger_event == null or not _is_valid_pantry_empty_trigger(trigger_event, npc_ids):
|
||||
return false
|
||||
var trigger_tick := int(trigger_event.data["tick"])
|
||||
var created_tick := opportunity.get_created_tick()
|
||||
if created_tick < trigger_tick or created_tick > current_tick:
|
||||
return false
|
||||
|
||||
var status := opportunity.get_status()
|
||||
var resolution_event_id := opportunity.get_resolution_event_id()
|
||||
var resolved_tick := opportunity.get_resolved_tick()
|
||||
if status == OpportunityStateRecord.STATUS_OPEN:
|
||||
var target_storage := (
|
||||
storage_records_by_id.get(opportunity.get_target_id()) as StorageStateRecord
|
||||
)
|
||||
var evidence_key := (
|
||||
"%d:%d" % [opportunity.get_interested_npc_id(), opportunity.get_trigger_event_id()]
|
||||
)
|
||||
var evidence := knowledge_records_by_key.get(evidence_key) as KnownEventStateRecord
|
||||
return (
|
||||
resolution_event_id == OpportunityStateRecord.NO_EVENT_ID
|
||||
and resolved_tick == -1
|
||||
and target_storage != null
|
||||
and (
|
||||
target_storage.get_amount(opportunity.get_resource_id())
|
||||
< opportunity.get_target_amount()
|
||||
)
|
||||
and evidence != null
|
||||
and evidence.get_acquired_tick() <= created_tick
|
||||
)
|
||||
if status != OpportunityStateRecord.STATUS_RESOLVED:
|
||||
return false
|
||||
if resolution_event_id == OpportunityStateRecord.NO_EVENT_ID:
|
||||
return false
|
||||
var resolution_event := event_records_by_id.get(resolution_event_id) as EconomicEventRecord
|
||||
if (
|
||||
resolution_event == null
|
||||
or not _is_valid_pantry_supply_resolution(resolution_event, npc_ids, resource_records_by_id)
|
||||
):
|
||||
return false
|
||||
var resolution_tick := int(resolution_event.data["tick"])
|
||||
return (
|
||||
resolution_event_id > opportunity.get_trigger_event_id()
|
||||
and resolution_tick >= created_tick
|
||||
and resolved_tick == resolution_tick
|
||||
and resolved_tick <= current_tick
|
||||
)
|
||||
|
||||
|
||||
static func _is_valid_pantry_empty_trigger(event: EconomicEventRecord, npc_ids: Dictionary) -> bool:
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
return (
|
||||
npc_ids.has(actor_id)
|
||||
and StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_WITHDRAWN
|
||||
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id)
|
||||
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||
and float(event.data["amount"]) > 0.0
|
||||
)
|
||||
|
||||
|
||||
static func _is_valid_pantry_supply_resolution(
|
||||
event: EconomicEventRecord, npc_ids: Dictionary, resource_records_by_id: Dictionary
|
||||
) -> bool:
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
var event_type := StringName(event.data["event_type"])
|
||||
if (
|
||||
StringName(event.data["destination_id"]) != SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
or StringName(event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
|
||||
or float(event.data["amount"]) <= 0.0
|
||||
):
|
||||
return false
|
||||
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
|
||||
return (
|
||||
npc_ids.has(actor_id)
|
||||
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id)
|
||||
)
|
||||
if event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED and actor_id == -1:
|
||||
var resource_state := (
|
||||
resource_records_by_id.get(StringName(event.data["source_id"])) as ResourceStateRecord
|
||||
)
|
||||
return (
|
||||
resource_state != null
|
||||
and resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||
and resource_state.can_player_use_resource()
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
|
||||
var migrated := legacy_data.duplicate(true)
|
||||
migrated["schema_version"] = SCHEMA_VERSION
|
||||
@@ -390,6 +559,10 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
||||
migrated.get("economic_events", []),
|
||||
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
|
||||
)
|
||||
migrated["opportunities"] = []
|
||||
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
|
||||
opportunity_simulation_data["next_opportunity_id"] = 0
|
||||
migrated["simulation"] = opportunity_simulation_data
|
||||
return migrated
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user