feat: unify authored activity commands
This commit is contained in:
@@ -86,6 +86,11 @@ var latest_decisions: Dictionary = {}
|
||||
var action_selector := ActionSelectionSystem.new()
|
||||
var action_executor := ActionExecutionSystem.new()
|
||||
var target_resolver := ActionTargetResolver.new()
|
||||
var activity_command_service: ActivityActionCommandService
|
||||
var interaction_service: CatalogInteractionService
|
||||
var _abstract_activity_registry: WorldTargetRegistry
|
||||
var _abstract_activity_command_service: ActivityActionCommandService
|
||||
var _active_activity_registry_instance_id := 0
|
||||
var last_player_resource_query_stats: Dictionary = {}
|
||||
var _population_view := SimulationPopulationView.new()
|
||||
var speed_index := 2
|
||||
@@ -129,6 +134,7 @@ func _ready() -> void:
|
||||
push_error("SimulationManager: " + error)
|
||||
set_process(false)
|
||||
return
|
||||
_configure_interaction_services()
|
||||
clock = SimulationClock.new(tick_interval)
|
||||
clock.cycle_duration_seconds = cycle_duration_seconds
|
||||
clock.elapsed_ticks = int(0.25 * cycle_duration_seconds / tick_interval)
|
||||
@@ -395,11 +401,21 @@ func _complete_current_action(npc: SimNPC) -> void:
|
||||
var completed_task := npc.current_task
|
||||
var definition := SimulationDefinitions.get_action(completed_task)
|
||||
var completion_succeeded := false
|
||||
if completed_task == SimulationIds.ACTION_FEED_ANIMAL:
|
||||
var completion_already_applied := false
|
||||
if _uses_activity_command_handler(definition):
|
||||
var activity_result := execute_activity_action(
|
||||
WorldEntityRef.create(SimulationIds.ENTITY_PERSON, StringName(str(npc.id))),
|
||||
completed_task,
|
||||
npc.target_id,
|
||||
get_action_state_revision()
|
||||
)
|
||||
completion_succeeded = activity_result != null and activity_result.did_succeed()
|
||||
completion_already_applied = completion_succeeded
|
||||
elif completed_task == SimulationIds.ACTION_FEED_ANIMAL:
|
||||
completion_succeeded = feed_animal(npc.target_id, npc.id)
|
||||
else:
|
||||
completion_succeeded = economy.consume_completion_cost(npc, definition)
|
||||
if completion_succeeded:
|
||||
if completion_succeeded and not completion_already_applied:
|
||||
_apply_action_completion(npc, completed_task, definition)
|
||||
elif not npc.target_id.is_empty():
|
||||
release_npc_reservation(npc.id)
|
||||
@@ -436,7 +452,7 @@ func _apply_action_completion(
|
||||
record_narrative_event(SimulationIds.EVENT_HOME_DAMAGED, npc.id)
|
||||
SimulationIds.ACTION_FEED_ANIMAL:
|
||||
pass
|
||||
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
||||
SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
||||
village.apply_npc_task(npc)
|
||||
SimulationIds.ACTION_DEFEND:
|
||||
village.apply_metric_delta(&"safety", 0.5)
|
||||
@@ -619,12 +635,24 @@ func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
|
||||
if active_world_adapter == null:
|
||||
push_error("SimulationManager: active_world_adapter is missing")
|
||||
return false
|
||||
var definition := SimulationDefinitions.get_action(npc.current_task)
|
||||
if _uses_activity_command_handler(definition) and not _ensure_interaction_services_current():
|
||||
return false
|
||||
var result := target_resolver.resolve(npc, origin, self, active_world_adapter)
|
||||
if result.is_empty():
|
||||
notify_npc_navigation_failed(npc.id)
|
||||
return false
|
||||
npc.target_id = StringName(result.get("target_id", ""))
|
||||
npc.travel_target_position = result["position"]
|
||||
if (
|
||||
definition != null
|
||||
and _uses_activity_command_handler(definition)
|
||||
and not _bind_abstract_activity_target(
|
||||
npc.target_id, npc.current_task, npc.travel_target_position
|
||||
)
|
||||
):
|
||||
notify_npc_navigation_failed(npc.id)
|
||||
return false
|
||||
npc.has_travel_target = true
|
||||
npc_travel_requested.emit(npc, npc.travel_target_position)
|
||||
return true
|
||||
@@ -661,6 +689,612 @@ func get_activity_target_claim_count(target_id: StringName, except_npc_id: int =
|
||||
return count
|
||||
|
||||
|
||||
func get_action_state_revision() -> int:
|
||||
return event_log.next_event_id
|
||||
|
||||
|
||||
func get_player_activity_offer(from_position: Vector3, max_distance: float) -> Dictionary:
|
||||
if not _ensure_interaction_services_current():
|
||||
return {}
|
||||
if (
|
||||
interaction_service == null
|
||||
or active_world_adapter == null
|
||||
or not from_position.is_finite()
|
||||
or not is_finite(max_distance)
|
||||
or max_distance <= 0.0
|
||||
or not active_world_adapter.has_method("get_target_registry")
|
||||
):
|
||||
return {}
|
||||
var registry := active_world_adapter.call("get_target_registry") as WorldTargetRegistry
|
||||
if registry == null:
|
||||
return {}
|
||||
var actor := WorldEntityRef.create(
|
||||
SimulationIds.ENTITY_PLAYER, StringName(str(SimulationIds.PLAYER_ACTOR_ID))
|
||||
)
|
||||
var candidates: Array[Dictionary] = []
|
||||
for descriptor in registry.get_descriptors(ActiveWorldAdapter.TARGET_KIND_ACTIVITY):
|
||||
var distance := from_position.distance_to(descriptor.get_local_position())
|
||||
if distance > max_distance:
|
||||
continue
|
||||
var handle := registry.get_handle(descriptor.get_target_id())
|
||||
for offer in interaction_service.get_offers(actor, handle):
|
||||
var definition := SimulationDefinitions.get_action(offer.get_action_id())
|
||||
if not _uses_activity_command_handler(definition):
|
||||
continue
|
||||
var capability := descriptor.get_capability(offer.get_action_id())
|
||||
var raw_range: Variant = capability.get_attribute(&"interaction_range", 0.0)
|
||||
if raw_range is not int and raw_range is not float:
|
||||
continue
|
||||
var capability_range := float(raw_range)
|
||||
if (
|
||||
not is_finite(capability_range)
|
||||
or capability_range <= 0.0
|
||||
or distance > capability_range
|
||||
):
|
||||
continue
|
||||
(
|
||||
candidates
|
||||
. append(
|
||||
{
|
||||
"action_id": String(offer.get_action_id()),
|
||||
"target_id": String(descriptor.get_target_id()),
|
||||
"display_name": descriptor.get_display_name(),
|
||||
"action_name": offer.get_display_name(),
|
||||
"distance": distance,
|
||||
"priority": offer.get_priority(),
|
||||
"state_revision": get_action_state_revision(),
|
||||
"target_generation": handle.get_generation(),
|
||||
"target_context_id": String(handle.get_context_id()),
|
||||
"target_registry_instance_id": registry.get_instance_id(),
|
||||
"target_kind": String(handle.get_target_kind()),
|
||||
"effect": _player_activity_effect_summary(definition),
|
||||
"enabled": offer.is_enabled(),
|
||||
"blocked_reason": offer.get_rejection_reason(),
|
||||
"offer_parameters": offer.get_parameters(),
|
||||
}
|
||||
)
|
||||
)
|
||||
if candidates.is_empty():
|
||||
return {}
|
||||
candidates.sort_custom(_activity_offer_precedes)
|
||||
return candidates[0].duplicate(true)
|
||||
|
||||
|
||||
func execute_activity_action(
|
||||
actor: WorldEntityRef,
|
||||
action_id: StringName,
|
||||
target_id: StringName,
|
||||
expected_state_revision: int = -1
|
||||
) -> ActionResult:
|
||||
if (
|
||||
actor == null
|
||||
or not actor.is_valid()
|
||||
or actor.get_entity_type() != SimulationIds.ENTITY_PERSON
|
||||
or expected_state_revision < 0
|
||||
):
|
||||
return ActionResult.rejected(&"activity_invalid_actor", &"invalid_actor")
|
||||
return _execute_authorized_activity_action(actor, action_id, target_id, expected_state_revision)
|
||||
|
||||
|
||||
func _execute_authorized_activity_action(
|
||||
actor: WorldEntityRef,
|
||||
action_id: StringName,
|
||||
target_id: StringName,
|
||||
expected_state_revision: int
|
||||
) -> ActionResult:
|
||||
if not _ensure_interaction_services_current():
|
||||
return ActionResult.rejected(&"activity_scope_mismatch", &"service_unavailable")
|
||||
var selected_service := activity_command_service
|
||||
var handle: WorldTargetHandle
|
||||
if actor.get_entity_type() == SimulationIds.ENTITY_PERSON:
|
||||
var npc_id_text := String(actor.get_entity_id())
|
||||
var npc := _find_npc_by_id(npc_id_text.to_int()) if npc_id_text.is_valid_int() else null
|
||||
if (
|
||||
npc == null
|
||||
or npc.is_dead
|
||||
or npc.task_state != SimNPC.TASK_STATE_COMPLETE
|
||||
or not npc.task_complete
|
||||
or npc.current_task != action_id
|
||||
or npc.target_id.is_empty()
|
||||
or npc.target_id != target_id
|
||||
):
|
||||
return ActionResult.rejected(
|
||||
&"activity_assignment_mismatch",
|
||||
ActivityActionCommandService.REASON_UNSUPPORTED_ACTOR
|
||||
)
|
||||
selected_service = _abstract_activity_command_service
|
||||
handle = _ensure_abstract_activity_handle(action_id, target_id)
|
||||
elif active_world_adapter != null and active_world_adapter.has_method("get_target_registry"):
|
||||
var registry := active_world_adapter.call("get_target_registry") as WorldTargetRegistry
|
||||
handle = registry.get_handle(target_id) if registry != null else null
|
||||
if selected_service == null or handle == null:
|
||||
return ActionResult.rejected(&"activity_unavailable", &"service_unavailable")
|
||||
var actor_key := actor.index_key() if actor != null else "invalid"
|
||||
var command_revision := get_action_state_revision()
|
||||
var command_id := StringName(
|
||||
(
|
||||
"activity_%s_%s_%s_%d_%d"
|
||||
% [
|
||||
actor_key.sha256_text().substr(0, 8),
|
||||
action_id,
|
||||
target_id,
|
||||
tick_count,
|
||||
command_revision,
|
||||
]
|
||||
)
|
||||
)
|
||||
return selected_service.submit_command(
|
||||
ActionCommand.new(command_id, actor, action_id, handle, {}, expected_state_revision)
|
||||
)
|
||||
|
||||
|
||||
func execute_player_activity_action(
|
||||
action_id: StringName,
|
||||
target_id: StringName,
|
||||
expected_state_revision: int = -1,
|
||||
expected_target_generation: int = -1,
|
||||
expected_context_id: StringName = &"",
|
||||
expected_registry_instance_id: int = 0
|
||||
) -> ActionResult:
|
||||
if not _ensure_interaction_services_current():
|
||||
return ActionResult.rejected(&"player_activity_stale", &"service_unavailable")
|
||||
if (
|
||||
expected_state_revision < 0
|
||||
or expected_target_generation < 1
|
||||
or expected_context_id.is_empty()
|
||||
or expected_registry_instance_id == 0
|
||||
or active_world_adapter == null
|
||||
or not active_world_adapter.has_method("get_target_registry")
|
||||
):
|
||||
return ActionResult.rejected(
|
||||
&"player_activity_stale",
|
||||
ActivityActionCommandService.REASON_STALE_TARGET,
|
||||
"The activity offer is incomplete; refresh it"
|
||||
)
|
||||
var registry := active_world_adapter.call("get_target_registry") as WorldTargetRegistry
|
||||
var current_handle := registry.get_handle(target_id) if registry != null else null
|
||||
if (
|
||||
current_handle == null
|
||||
or registry.get_instance_id() != expected_registry_instance_id
|
||||
or current_handle.get_generation() != expected_target_generation
|
||||
or current_handle.get_context_id() != expected_context_id
|
||||
or current_handle.get_target_kind() != SimulationIds.TARGET_ACTIVITY
|
||||
):
|
||||
return ActionResult.rejected(
|
||||
&"player_activity_stale",
|
||||
ActivityActionCommandService.REASON_STALE_TARGET,
|
||||
"The activity changed; refresh the interaction offer"
|
||||
)
|
||||
return _execute_authorized_activity_action(
|
||||
WorldEntityRef.create(
|
||||
SimulationIds.ENTITY_PLAYER, StringName(str(SimulationIds.PLAYER_ACTOR_ID))
|
||||
),
|
||||
action_id,
|
||||
target_id,
|
||||
expected_state_revision
|
||||
)
|
||||
|
||||
|
||||
func _configure_interaction_services() -> bool:
|
||||
activity_command_service = null
|
||||
interaction_service = null
|
||||
_active_activity_registry_instance_id = 0
|
||||
var current_scope := event_log.get_scope()
|
||||
var scope_world_id := StringName(
|
||||
current_scope.get("world_id", SimulationIds.REGIONAL_WORLD_BOSNIA)
|
||||
)
|
||||
var scope_location_id := StringName(
|
||||
current_scope.get("location_id", SimulationIds.REGIONAL_LOCATION_JAJCE)
|
||||
)
|
||||
if active_world_adapter != null and active_world_adapter.has_method("get_context_identity"):
|
||||
var identity: Dictionary = active_world_adapter.call("get_context_identity")
|
||||
scope_world_id = StringName(identity.get("world_id", scope_world_id))
|
||||
scope_location_id = StringName(identity.get("location_id", scope_location_id))
|
||||
if not event_log.configure_scope(scope_world_id, scope_location_id):
|
||||
return false
|
||||
_abstract_activity_registry = WorldTargetRegistry.new(
|
||||
&"simulation_jajce", scope_world_id, scope_location_id
|
||||
)
|
||||
_abstract_activity_command_service = ActivityActionCommandService.new(
|
||||
_abstract_activity_registry,
|
||||
SimulationDefinitions.get_action,
|
||||
_get_activity_actor_position,
|
||||
get_action_state_revision,
|
||||
_complete_activity_command,
|
||||
false,
|
||||
false
|
||||
)
|
||||
register_abstract_activity_target(
|
||||
SimulationIds.ACTIVITY_GUARD_POST, SimulationIds.ACTION_PATROL, Vector3.ZERO
|
||||
)
|
||||
register_abstract_activity_target(
|
||||
SimulationIds.ACTIVITY_STUDY_DESK, SimulationIds.ACTION_STUDY, Vector3.ZERO
|
||||
)
|
||||
_rebuild_abstract_activity_targets_from_npcs()
|
||||
var catalog := ContentCatalog.create_core()
|
||||
if not catalog.is_valid():
|
||||
return false
|
||||
if active_world_adapter == null or not active_world_adapter.has_method("get_target_registry"):
|
||||
return true
|
||||
var registry := active_world_adapter.call("get_target_registry") as WorldTargetRegistry
|
||||
if registry == null:
|
||||
return false
|
||||
activity_command_service = ActivityActionCommandService.new(
|
||||
registry,
|
||||
SimulationDefinitions.get_action,
|
||||
_get_activity_actor_position,
|
||||
get_action_state_revision,
|
||||
_complete_activity_command,
|
||||
true
|
||||
)
|
||||
interaction_service = CatalogInteractionService.new(
|
||||
registry, catalog, _evaluate_activity_availability, true
|
||||
)
|
||||
_active_activity_registry_instance_id = registry.get_instance_id()
|
||||
return true
|
||||
|
||||
|
||||
func register_abstract_activity_target(
|
||||
target_id: StringName, action_id: StringName, position: Vector3, interaction_range: float = 0.5
|
||||
) -> bool:
|
||||
if (
|
||||
_abstract_activity_registry == null
|
||||
or target_id.is_empty()
|
||||
or not position.is_finite()
|
||||
or not is_finite(interaction_range)
|
||||
or interaction_range <= 0.0
|
||||
or _abstract_activity_registry.has_target(target_id)
|
||||
):
|
||||
return false
|
||||
var definition := SimulationDefinitions.get_action(action_id)
|
||||
if not _uses_activity_command_handler(definition):
|
||||
return false
|
||||
var scope := _abstract_activity_registry.get_identity()
|
||||
var handle := (
|
||||
_abstract_activity_registry
|
||||
. register_target(
|
||||
(
|
||||
WorldTargetDescriptor
|
||||
. new(
|
||||
target_id,
|
||||
ActiveWorldAdapter.TARGET_KIND_ACTIVITY,
|
||||
[
|
||||
WorldTargetCapability.new(
|
||||
action_id, {"interaction_range": interaction_range}
|
||||
)
|
||||
],
|
||||
position,
|
||||
definition.display_name,
|
||||
{
|
||||
"authority": "simulation",
|
||||
"world_id": String(scope["world_id"]),
|
||||
"location_id": String(scope["location_id"]),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return handle != null
|
||||
|
||||
|
||||
func _bind_abstract_activity_target(
|
||||
target_id: StringName, action_id: StringName, position: Vector3
|
||||
) -> bool:
|
||||
if _abstract_activity_registry == null or target_id.is_empty() or not position.is_finite():
|
||||
return false
|
||||
var existing := _abstract_activity_registry.get_handle(target_id)
|
||||
if existing == null:
|
||||
return register_abstract_activity_target(target_id, action_id, position)
|
||||
var descriptor := _abstract_activity_registry.resolve_handle(existing)
|
||||
if (
|
||||
descriptor == null
|
||||
or descriptor.get_target_kind() != SimulationIds.TARGET_ACTIVITY
|
||||
or not descriptor.has_capability(action_id)
|
||||
):
|
||||
return false
|
||||
return _abstract_activity_registry.update_target_position(existing, position)
|
||||
|
||||
|
||||
func _rebuild_abstract_activity_targets_from_npcs() -> void:
|
||||
for npc in npcs:
|
||||
if npc.is_dead or npc.target_id.is_empty():
|
||||
continue
|
||||
var definition := SimulationDefinitions.get_action(npc.current_task)
|
||||
if not _uses_activity_command_handler(definition):
|
||||
continue
|
||||
_bind_abstract_activity_target(npc.target_id, npc.current_task, npc.travel_target_position)
|
||||
|
||||
|
||||
func _ensure_interaction_services_current() -> bool:
|
||||
if active_world_adapter == null or not active_world_adapter.has_method("get_target_registry"):
|
||||
return _abstract_activity_command_service != null
|
||||
var registry := active_world_adapter.call("get_target_registry") as WorldTargetRegistry
|
||||
if registry == null:
|
||||
return false
|
||||
var identity := registry.get_identity()
|
||||
var event_scope := event_log.get_scope()
|
||||
if (
|
||||
StringName(identity["world_id"]) != StringName(event_scope["world_id"])
|
||||
or StringName(identity["location_id"]) != StringName(event_scope["location_id"])
|
||||
):
|
||||
if not economic_events.is_empty():
|
||||
activity_command_service = null
|
||||
interaction_service = null
|
||||
return false
|
||||
if registry.get_instance_id() == _active_activity_registry_instance_id:
|
||||
return activity_command_service != null and _abstract_activity_command_service != null
|
||||
return _configure_interaction_services()
|
||||
|
||||
|
||||
func _ensure_abstract_activity_handle(
|
||||
action_id: StringName, requested_target_id: StringName
|
||||
) -> WorldTargetHandle:
|
||||
if _abstract_activity_registry == null:
|
||||
return null
|
||||
var target_id := requested_target_id
|
||||
if target_id.is_empty():
|
||||
var candidates := _abstract_activity_registry.get_target_ids(
|
||||
ActiveWorldAdapter.TARGET_KIND_ACTIVITY, action_id
|
||||
)
|
||||
if candidates.size() != 1:
|
||||
return null
|
||||
target_id = candidates[0]
|
||||
var existing := _abstract_activity_registry.get_handle(target_id)
|
||||
if existing == null:
|
||||
return null
|
||||
var descriptor := _abstract_activity_registry.resolve_handle(existing)
|
||||
if descriptor == null or not descriptor.has_capability(action_id):
|
||||
return null
|
||||
return _abstract_activity_registry.get_handle(target_id)
|
||||
|
||||
|
||||
func _evaluate_activity_availability(
|
||||
actor: WorldEntityRef,
|
||||
_descriptor: WorldTargetDescriptor,
|
||||
definition: ActionDefinition,
|
||||
_capability: WorldTargetCapability
|
||||
) -> InteractionAvailabilityDecision:
|
||||
if not _uses_activity_command_handler(definition):
|
||||
return InteractionAvailabilityDecision.denied(
|
||||
&"unsupported_handler",
|
||||
"This activity has no supported completion handler",
|
||||
[&"handler_missing"]
|
||||
)
|
||||
var effect := ActivityActionCommandService._metric_effect(definition)
|
||||
var actor_id := _activity_actor_id(actor)
|
||||
if effect == null or actor_id < SimulationIds.PLAYER_ACTOR_ID:
|
||||
return InteractionAvailabilityDecision.denied(
|
||||
&"invalid_effect", "This activity has no valid effect", [&"effect_rejected"]
|
||||
)
|
||||
var requested_amount := _activity_effect_amount(actor_id, effect.value, effect.parameters)
|
||||
if _bounded_activity_effect_amount(effect.subject_key, requested_amount) <= 0.0:
|
||||
return InteractionAvailabilityDecision.denied(
|
||||
&"no_effect", "This activity cannot improve the village further", [&"effect_capped"]
|
||||
)
|
||||
if definition.has_completion_cost():
|
||||
var storage := economy.get_storage_for_resource(definition.completion_cost_resource_id)
|
||||
var available := (
|
||||
storage.get_amount(definition.completion_cost_resource_id) if storage != null else 0.0
|
||||
)
|
||||
if available < definition.completion_cost_amount:
|
||||
return InteractionAvailabilityDecision.denied(
|
||||
&"insufficient_cost",
|
||||
(
|
||||
"Needs %.0f %s"
|
||||
% [definition.completion_cost_amount, definition.completion_cost_resource_id]
|
||||
),
|
||||
[&"cost_rejected"],
|
||||
{"available": available, "required": definition.completion_cost_amount}
|
||||
)
|
||||
return InteractionAvailabilityDecision.allowed([&"cost_allowed"])
|
||||
|
||||
|
||||
func _get_activity_actor_position(actor: WorldEntityRef) -> Variant:
|
||||
var actor_id := _activity_actor_id(actor)
|
||||
if actor_id == SimulationIds.PLAYER_ACTOR_ID:
|
||||
var combatant := conflict_system.get_combatant(ConflictSystemScript.PLAYER_COMBATANT_ID)
|
||||
return combatant.get_position() if combatant != null else null
|
||||
var npc := _find_npc_by_id(actor_id)
|
||||
return npc.position if npc != null and not npc.is_dead else null
|
||||
|
||||
|
||||
func _complete_activity_command(
|
||||
actor: WorldEntityRef,
|
||||
action_id: StringName,
|
||||
target_id: StringName,
|
||||
metric_id: StringName,
|
||||
base_amount: float,
|
||||
effect_parameters: Dictionary,
|
||||
world_position: Vector3
|
||||
) -> Dictionary:
|
||||
var actor_id := _activity_actor_id(actor)
|
||||
var definition := SimulationDefinitions.get_action(action_id)
|
||||
if actor_id < SimulationIds.PLAYER_ACTOR_ID or not _uses_activity_command_handler(definition):
|
||||
return {"succeeded": false, "event_id": -1, "state_revision": get_action_state_revision()}
|
||||
var requested_amount := _activity_effect_amount(actor_id, base_amount, effect_parameters)
|
||||
var applied_amount := _bounded_activity_effect_amount(metric_id, requested_amount)
|
||||
if not is_finite(applied_amount) or applied_amount <= 0.0:
|
||||
return {
|
||||
"succeeded": false,
|
||||
"reason_code": "no_effect",
|
||||
"message": "This activity cannot improve the village further",
|
||||
"event_id": -1,
|
||||
"state_revision": get_action_state_revision(),
|
||||
}
|
||||
var cost_storage: StorageStateRecord
|
||||
var cost_item_id: StringName
|
||||
var cost_amount := 0.0
|
||||
if definition.has_completion_cost():
|
||||
cost_item_id = definition.completion_cost_resource_id
|
||||
cost_amount = definition.completion_cost_amount
|
||||
cost_storage = economy.get_storage_for_resource(cost_item_id)
|
||||
var available := cost_storage.get_amount(cost_item_id) if cost_storage != null else 0.0
|
||||
if available < cost_amount:
|
||||
var blocked := event_log.record_activity_blocked(
|
||||
tick_count,
|
||||
actor_id,
|
||||
target_id,
|
||||
action_id,
|
||||
(
|
||||
"%s: needs %.0f %s"
|
||||
% [definition.display_name, cost_amount, String(cost_item_id).capitalize()]
|
||||
),
|
||||
world_position,
|
||||
cost_storage.get_storage_id() if cost_storage != null else &"",
|
||||
cost_item_id,
|
||||
cost_amount
|
||||
)
|
||||
return {
|
||||
"succeeded": false,
|
||||
"reason_code": "insufficient_cost",
|
||||
"message": "Needs %.0f %s" % [cost_amount, cost_item_id],
|
||||
"event_id": int(blocked.data["event_id"]) if blocked != null else -1,
|
||||
"state_revision": get_action_state_revision(),
|
||||
}
|
||||
var previous_metric := village.safety if metric_id == &"safety" else village.knowledge
|
||||
if cost_storage != null and cost_storage.withdraw(cost_item_id, cost_amount) < cost_amount:
|
||||
return {"succeeded": false, "event_id": -1, "state_revision": get_action_state_revision()}
|
||||
if cost_storage != null:
|
||||
economy.sync_resource(cost_item_id)
|
||||
village.apply_metric_delta(metric_id, applied_amount)
|
||||
var actual_amount := (
|
||||
village.safety - previous_metric
|
||||
if metric_id == &"safety"
|
||||
else village.knowledge - previous_metric
|
||||
)
|
||||
if not is_finite(actual_amount) or actual_amount <= 0.0:
|
||||
if cost_storage != null:
|
||||
cost_storage.deposit(cost_item_id, cost_amount)
|
||||
economy.sync_resource(cost_item_id)
|
||||
_restore_village_metric(metric_id, previous_metric)
|
||||
return {
|
||||
"succeeded": false,
|
||||
"reason_code": "no_effect",
|
||||
"message": "This activity cannot improve the village further",
|
||||
"event_id": -1,
|
||||
"state_revision": get_action_state_revision(),
|
||||
}
|
||||
var event := event_log.record_activity_completed(
|
||||
tick_count,
|
||||
actor_id,
|
||||
target_id,
|
||||
action_id,
|
||||
definition.event_type_id,
|
||||
metric_id,
|
||||
actual_amount,
|
||||
world_position,
|
||||
cost_item_id,
|
||||
cost_amount
|
||||
)
|
||||
if event == null:
|
||||
if cost_storage != null:
|
||||
cost_storage.deposit(cost_item_id, cost_amount)
|
||||
economy.sync_resource(cost_item_id)
|
||||
_restore_village_metric(metric_id, previous_metric)
|
||||
return {"succeeded": false, "event_id": -1, "state_revision": get_action_state_revision()}
|
||||
village_changed.emit(village)
|
||||
return {
|
||||
"succeeded": true,
|
||||
"event_id": int(event.data["event_id"]),
|
||||
"state_revision": get_action_state_revision(),
|
||||
"applied_amount": actual_amount,
|
||||
}
|
||||
|
||||
|
||||
func _activity_actor_id(actor: WorldEntityRef) -> int:
|
||||
if actor == null or not actor.is_valid():
|
||||
return SimulationIds.PLAYER_ACTOR_ID - 1
|
||||
var id_text := String(actor.get_entity_id())
|
||||
if not id_text.is_valid_int():
|
||||
return SimulationIds.PLAYER_ACTOR_ID - 1
|
||||
var actor_id := id_text.to_int()
|
||||
if actor.get_entity_type() == SimulationIds.ENTITY_PLAYER:
|
||||
return (
|
||||
actor_id
|
||||
if (
|
||||
actor_id == SimulationIds.PLAYER_ACTOR_ID
|
||||
and player_system.player_state != null
|
||||
and not player_system.player_state.is_downed()
|
||||
)
|
||||
else -2
|
||||
)
|
||||
if actor.get_entity_type() != SimulationIds.ENTITY_PERSON or actor_id < 0:
|
||||
return -2
|
||||
var npc := _find_npc_by_id(actor_id)
|
||||
return actor_id if npc != null and not npc.is_dead else -2
|
||||
|
||||
|
||||
func _activity_effect_amount(actor_id: int, base_amount: float, parameters: Dictionary) -> float:
|
||||
var result := base_amount
|
||||
if actor_id == SimulationIds.PLAYER_ACTOR_ID:
|
||||
var raw_multiplier: Variant = parameters.get("player_multiplier", 1.0)
|
||||
if raw_multiplier is int or raw_multiplier is float:
|
||||
result *= float(raw_multiplier)
|
||||
return result
|
||||
if bool(parameters.get("npc_productivity", false)):
|
||||
var npc := _find_npc_by_id(actor_id)
|
||||
if npc == null:
|
||||
return 0.0
|
||||
if npc.is_starving:
|
||||
result *= 0.35
|
||||
elif npc.energy < 20.0:
|
||||
result *= 0.25
|
||||
elif npc.energy < 40.0:
|
||||
result *= 0.5
|
||||
return result
|
||||
|
||||
|
||||
func _player_activity_effect_summary(definition: ActionDefinition) -> Dictionary:
|
||||
if definition == null:
|
||||
return {}
|
||||
var effect := ActivityActionCommandService._metric_effect(definition)
|
||||
if effect == null:
|
||||
return {}
|
||||
var requested_amount := _activity_effect_amount(
|
||||
SimulationIds.PLAYER_ACTOR_ID, effect.value, effect.parameters
|
||||
)
|
||||
return {
|
||||
"metric_id": String(effect.subject_key),
|
||||
"amount": _bounded_activity_effect_amount(effect.subject_key, requested_amount),
|
||||
}
|
||||
|
||||
|
||||
func _bounded_activity_effect_amount(metric_id: StringName, requested_amount: float) -> float:
|
||||
if not is_finite(requested_amount) or requested_amount <= 0.0:
|
||||
return 0.0
|
||||
if metric_id == &"safety":
|
||||
return minf(requested_amount, maxf(100.0 - village.safety, 0.0))
|
||||
if metric_id == &"knowledge":
|
||||
return requested_amount
|
||||
return 0.0
|
||||
|
||||
|
||||
func _restore_village_metric(metric_id: StringName, value: float) -> void:
|
||||
if metric_id == &"safety":
|
||||
village.safety = value
|
||||
elif metric_id == &"knowledge":
|
||||
village.knowledge = value
|
||||
village.update_modifiers()
|
||||
village.update_priorities()
|
||||
|
||||
|
||||
static func _uses_activity_command_handler(definition: ActionDefinition) -> bool:
|
||||
return (
|
||||
definition != null
|
||||
and definition.completion_handler_id == ActivityActionCommandService.HANDLER_METRIC_DELTA
|
||||
)
|
||||
|
||||
|
||||
static func _activity_offer_precedes(first: Dictionary, second: Dictionary) -> bool:
|
||||
if not is_equal_approx(float(first["distance"]), float(second["distance"])):
|
||||
return float(first["distance"]) < float(second["distance"])
|
||||
if float(first["priority"]) != float(second["priority"]):
|
||||
return float(first["priority"]) > float(second["priority"])
|
||||
if String(first["target_id"]) != String(second["target_id"]):
|
||||
return String(first["target_id"]) < String(second["target_id"])
|
||||
return String(first["action_id"]) < String(second["action_id"])
|
||||
|
||||
|
||||
func _notify_mourning(dead_npc: SimNPC) -> void:
|
||||
var mourner := relationship_system.get_most_familiar_living(dead_npc.id, npcs, _population_view)
|
||||
if mourner == null:
|
||||
@@ -1653,6 +2287,7 @@ func get_latest_decision(npc_id: int) -> ActionSelectionResult:
|
||||
|
||||
func create_state_record() -> SimulationStateRecord:
|
||||
var record := SimulationStateRecord.new()
|
||||
var event_scope := event_log.get_scope()
|
||||
var wander_streams: Array[Dictionary] = []
|
||||
var sorted_npc_ids: Array = wander_random_sources.keys()
|
||||
sorted_npc_ids.sort()
|
||||
@@ -1674,6 +2309,8 @@ func create_state_record() -> SimulationStateRecord:
|
||||
"next_journal_entry_id": quest_journal_system.get_next_entry_id(),
|
||||
"next_commitment_id": commitment_system.get_next_commitment_id(),
|
||||
"next_conversation_act_id": _next_conversation_act_id,
|
||||
"world_id": String(event_scope["world_id"]),
|
||||
"location_id": String(event_scope["location_id"]),
|
||||
"cycle_duration_seconds": clock.cycle_duration_seconds
|
||||
}
|
||||
record.village = VillageStateRecord.capture(village)
|
||||
@@ -1724,7 +2361,7 @@ func restore_state_from_json(json_text: String) -> bool:
|
||||
|
||||
|
||||
func restore_state(record: SimulationStateRecord) -> bool:
|
||||
if record == null:
|
||||
if record == null or not _saved_scope_matches_active_adapter(record):
|
||||
return false
|
||||
simulation_seed = int(record.simulation["seed"])
|
||||
tick_interval = float(record.simulation["tick_interval"])
|
||||
@@ -1740,12 +2377,20 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
# Rebuild them before the next decision so save/load cannot change outcomes.
|
||||
village.update_modifiers()
|
||||
village.update_priorities()
|
||||
event_log.restore(record.economic_events, int(record.simulation["next_event_id"]))
|
||||
if not event_log.restore(
|
||||
record.economic_events,
|
||||
int(record.simulation["next_event_id"]),
|
||||
StringName(record.simulation["world_id"]),
|
||||
StringName(record.simulation["location_id"])
|
||||
):
|
||||
return false
|
||||
latest_decisions.clear()
|
||||
npcs.clear()
|
||||
for npc_record in record.npcs:
|
||||
npcs.append(npc_record.restore(debug_logs))
|
||||
refresh_population_index()
|
||||
if not _configure_interaction_services():
|
||||
return false
|
||||
relationship_system.restore(record.relationships)
|
||||
event_knowledge_system.restore(record.event_knowledge)
|
||||
opportunity_system.restore(record.opportunities, int(record.simulation["next_opportunity_id"]))
|
||||
@@ -1798,3 +2443,19 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
village_changed.emit(village)
|
||||
state_restored.emit()
|
||||
return true
|
||||
|
||||
|
||||
func _saved_scope_matches_active_adapter(record: SimulationStateRecord) -> bool:
|
||||
if active_world_adapter == null or not active_world_adapter.has_method("get_context_identity"):
|
||||
return true
|
||||
var identity: Dictionary = active_world_adapter.call("get_context_identity")
|
||||
return (
|
||||
(
|
||||
StringName(identity.get("world_id", &""))
|
||||
== StringName(record.simulation.get("world_id", &""))
|
||||
)
|
||||
and (
|
||||
StringName(identity.get("location_id", &""))
|
||||
== StringName(record.simulation.get("location_id", &""))
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user