feat: surface player routes for village needs
This commit is contained in:
@@ -48,7 +48,10 @@ would otherwise obscure that lifecycle:
|
|||||||
work wood consumers. It also derives one ephemeral `OpportunityHelperResult`
|
work wood consumers. It also derives one ephemeral `OpportunityHelperResult`
|
||||||
from knowledge, directed relationships, inventory, action definitions, and
|
from knowledge, directed relationships, inventory, action definitions, and
|
||||||
finite-resource state without changing resources or assigning tasks. The
|
finite-resource state without changing resources or assigning tasks. The
|
||||||
ordinary action selector consumes that result only for the matching idle NPC;
|
ordinary action selector consumes that result only for the matching idle NPC.
|
||||||
|
When no such helper exists, it can separately derive an ephemeral
|
||||||
|
`OpportunityPlayerResponseResult` from the real destination capacity and
|
||||||
|
enabled player-usable finite sources;
|
||||||
- `simulation/persistence/` owns save-slot file safety;
|
- `simulation/persistence/` owns save-slot file safety;
|
||||||
- `simulation/state/` owns versioned serialized record contracts;
|
- `simulation/state/` owns versioned serialized record contracts;
|
||||||
- `simulation/definitions/` owns stable IDs and immutable action/profession
|
- `simulation/definitions/` owns stable IDs and immutable action/profession
|
||||||
@@ -98,8 +101,9 @@ improving ownership.
|
|||||||
Relationship cues, interested-villager concern, physical pantry stock, and
|
Relationship cues, interested-villager concern, physical pantry stock, and
|
||||||
refill feedback consume authoritative state or state-change signals and are
|
refill feedback consume authoritative state or state-change signals and are
|
||||||
intentionally absent from saves and checksums. The transient village-whisper
|
intentionally absent from saves and checksums. The transient village-whisper
|
||||||
HUD likewise formats need, transfer, response, and resolution signals without
|
HUD likewise formats need, player route, transfer, response, and resolution
|
||||||
owning them. Stable visuals are rebuilt from state after restore; transient
|
signals without owning them. It drops the player route when a capable helper
|
||||||
|
emerges. Stable visuals are rebuilt from state after restore; transient
|
||||||
reactions are not replayed.
|
reactions are not replayed.
|
||||||
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
- Resource changes go through `ResourceStateRecord`, NPC inventory, and
|
||||||
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
`VillageEconomy`; `village.food` and `village.wood` are synchronized views.
|
||||||
|
|||||||
@@ -746,13 +746,17 @@ Completed:
|
|||||||
informed trusted helper to respond autonomously. One cardless warm-amber HUD
|
informed trusted helper to respond autonomously. One cardless warm-amber HUD
|
||||||
ribbon gently rises, pulses, and fades through need, named report, response,
|
ribbon gently rises, pulses, and fades through need, named report, response,
|
||||||
and resolution beats while remaining separate from development overlays.
|
and resolution beats while remaining separate from development overlays.
|
||||||
|
31. Player response whisper: when no capable helper exists, the same ribbon now
|
||||||
|
derives one concise ordinary harvest route from current player-usable finite
|
||||||
|
resources and the real pantry or woodpile. It clears when help emerges or
|
||||||
|
the need closes, and adds no quest, waypoint, tracker, or saved UI state.
|
||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. When no capable helper exists, show one contextual player response hint
|
1. Begin Milestone 8 with a deterministic headless scaling baseline over
|
||||||
derived from a current player-usable finite source and the real storage
|
increasing NPC and event counts. Measure current tick throughput,
|
||||||
target. Clear it as soon as help emerges or the need closes; do not add quest
|
serialized-state growth, and retained history before choosing a target
|
||||||
acceptance, waypoint state, or a generic tracker, as sequenced in
|
population or adding spatial partitions and simulation LOD, as sequenced in
|
||||||
`LEARNING_ROADMAP.md`.
|
`LEARNING_ROADMAP.md`.
|
||||||
|
|
||||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||||
|
|||||||
@@ -822,12 +822,38 @@ The first direct information-to-help path is complete:
|
|||||||
helper response, and resolution. It is signal-driven, survives debug-overlay
|
helper response, and resolution. It is signal-driven, survives debug-overlay
|
||||||
hiding, and neither serializes nor replays after restore.
|
hiding, and neither serializes nor replays after restore.
|
||||||
|
|
||||||
Milestone 7 is not complete. The immediate next slice should expose one bounded
|
The bounded Milestone 7 player-response affordance is complete:
|
||||||
player response affordance when an open need has no capable helper: derive a
|
|
||||||
concise contextual hint from the existing player-usable finite source and real
|
- an open need with no capable helper derives one ephemeral
|
||||||
storage target, then clear it when a helper emerges or the need closes. Reuse
|
`OpportunityPlayerResponseResult` only when its real storage target has room
|
||||||
the current interaction/economy rules; do not add quest acceptance, waypoint
|
and at least one matching enabled, stocked, player-usable finite source
|
||||||
state, or a generic objective tracker.
|
exists;
|
||||||
|
- the result exposes the ordinary gather action, resource, destination, source
|
||||||
|
count, opportunity, and trigger IDs without selecting a waypoint, accepting a
|
||||||
|
quest, drawing RNG, or mutating simulation state;
|
||||||
|
- the existing cardless village-whisper ribbon says `YOU CAN HELP` and explains
|
||||||
|
the direct resource-to-storage interaction in one line, while the development
|
||||||
|
summary exposes the exact derived route;
|
||||||
|
- knowledge, relationship, inventory, or village-resource changes track the
|
||||||
|
derived route in both directions: a newly unassisted need can surface the
|
||||||
|
hint once, a capable helper removes it immediately, and resolution or
|
||||||
|
invalidation replaces it with the existing close beat;
|
||||||
|
- reserved resources remain honestly player-usable because the current player
|
||||||
|
extraction contract does not consume NPC reservations, while disabled,
|
||||||
|
depleted, wrong-resource, and capacity-blocked sources do not qualify;
|
||||||
|
- unit, headless lifecycle, restore/checksum, real player extraction, and Jajce
|
||||||
|
runtime regressions cover the slice without persisting or replaying HUD state.
|
||||||
|
|
||||||
|
This completes the bounded Milestone 7 simulation-garden proof. A known and
|
||||||
|
cared-about shortage now produces an inspectable opportunity whose NPC and
|
||||||
|
player responses both use the originating resource, relationship, knowledge,
|
||||||
|
and event systems rather than quest-only duplicates.
|
||||||
|
|
||||||
|
The immediate next slice should begin Milestone 8 with a deterministic headless
|
||||||
|
scaling baseline: measure tick throughput, serialized-state growth, and retained
|
||||||
|
history across increasing NPC/event counts using the current full-fidelity
|
||||||
|
simulation. Set an evidence-backed target before adding spatial partitions,
|
||||||
|
batching, or active/abstract LOD transitions.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
|
|||||||
+12
-5
@@ -935,11 +935,18 @@ re-derived and autonomously responds through ordinary action selection. A
|
|||||||
single cardless warm-amber HUD ribbon presents the real need, named report,
|
single cardless warm-amber HUD ribbon presents the real need, named report,
|
||||||
helper response, and resolution without entering simulation or save state.
|
helper response, and resolution without entering simulation or save state.
|
||||||
|
|
||||||
Milestone 7 remains in progress. The immediate next slice should add one
|
The bounded Milestone 7 player response is complete. When no capable helper
|
||||||
bounded player response hint only when no capable helper exists, derived from
|
exists, an ephemeral query now proves an enabled player-usable finite source and
|
||||||
the current player-usable finite source and real destination. It should clear
|
real destination with capacity. The warm-amber whisper briefly explains the
|
||||||
when help emerges or the need closes, without quest acceptance, waypoint state,
|
ordinary direct harvest route, then yields immediately when a helper emerges or
|
||||||
or a generic tracker.
|
the need closes. The result and HUD add no quest acceptance, waypoint, tracker,
|
||||||
|
save field, RNG draw, or simulation mutation. This completes the Milestone 7
|
||||||
|
simulation-garden exit proof.
|
||||||
|
|
||||||
|
The next slice should start Milestone 8 with a deterministic headless scaling
|
||||||
|
baseline over increasing NPC and event counts. Measure tick throughput,
|
||||||
|
serialized-state growth, and retained history before selecting a target
|
||||||
|
population or implementing spatial partitions and active/abstract LOD.
|
||||||
|
|
||||||
The remaining simulation-garden target still aims for:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -196,10 +196,13 @@ lasting during deterministic memory maintenance. Resolution or invalidation
|
|||||||
releases that fact back to the normal bounded-retention rules; neither deletes
|
releases that fact back to the normal bounded-retention rules; neither deletes
|
||||||
objective event or opportunity history.
|
objective event or opportunity history.
|
||||||
|
|
||||||
`OpportunityHelperResult` is deliberately absent from this schema. It is
|
`OpportunityHelperResult` and `OpportunityPlayerResponseResult` are deliberately
|
||||||
re-derived from the open opportunity plus current NPC knowledge, directed
|
absent from this schema. The helper result is re-derived from the open
|
||||||
relationship, inventory, action-definition, storage-capacity, and finite-
|
opportunity plus current NPC knowledge, directed relationship, inventory,
|
||||||
resource records. Save/restore regressions require the same helper/action/route
|
action-definition, storage-capacity, and finite-resource records. The player
|
||||||
|
result additionally requires no capable helper and derives its ordinary gather
|
||||||
|
action, real destination, and source count from current enabled player-usable
|
||||||
|
finite resources. Save/restore regressions require the same applicable route
|
||||||
result and an unchanged checksum before and after querying.
|
result and an unchanged checksum before and after querying.
|
||||||
|
|
||||||
When an idle derived helper chooses the reported supply action, only the
|
When an idle derived helper chooses the reported supply action, only the
|
||||||
@@ -209,8 +212,8 @@ and independently re-derives current capability from restored facts.
|
|||||||
|
|
||||||
Active-need communication adds no schema fields. It persists through the
|
Active-need communication adds no schema fields. It persists through the
|
||||||
existing `KnownEventStateRecord` event, listener, source NPC, direct source
|
existing `KnownEventStateRecord` event, listener, source NPC, direct source
|
||||||
method, and acquisition-tick fields. Communication/HUD animations are transient
|
method, and acquisition-tick fields. Communication and player-response HUD
|
||||||
and are cleared rather than replayed after restore.
|
animations are transient and are cleared rather than replayed after restore.
|
||||||
|
|
||||||
## Resource authority
|
## Resource authority
|
||||||
|
|
||||||
|
|||||||
@@ -782,24 +782,14 @@ func get_relationship_cause(relationship: RelationshipStateRecord) -> EconomicEv
|
|||||||
|
|
||||||
|
|
||||||
func get_known_events(npc_id: int, max_count: int = 3) -> Array[EconomicEventRecord]:
|
func get_known_events(npc_id: int, max_count: int = 3) -> Array[EconomicEventRecord]:
|
||||||
var known: Array[EconomicEventRecord] = []
|
return event_log.get_by_ids(event_knowledge_system.get_known_event_ids(npc_id, max_count))
|
||||||
for event_id in event_knowledge_system.get_known_event_ids(npc_id, max_count):
|
|
||||||
var event := event_log.get_by_id(event_id)
|
|
||||||
if event != null:
|
|
||||||
known.append(event)
|
|
||||||
return known
|
|
||||||
|
|
||||||
|
|
||||||
func get_retained_memory_events(npc_id: int, max_count: int = 4) -> Array[EconomicEventRecord]:
|
func get_retained_memory_events(npc_id: int, max_count: int = 4) -> Array[EconomicEventRecord]:
|
||||||
var retained: Array[EconomicEventRecord] = []
|
|
||||||
var event_ids := event_knowledge_system.get_retained_event_ids(
|
var event_ids := event_knowledge_system.get_retained_event_ids(
|
||||||
npc_id, _get_lasting_event_ids(npc_id), max_count
|
npc_id, _get_lasting_event_ids(npc_id), max_count
|
||||||
)
|
)
|
||||||
for event_id in event_ids:
|
return event_log.get_by_ids(event_ids)
|
||||||
var event := event_log.get_by_id(event_id)
|
|
||||||
if event != null:
|
|
||||||
retained.append(event)
|
|
||||||
return retained
|
|
||||||
|
|
||||||
|
|
||||||
func get_active_opportunity() -> OpportunityStateRecord:
|
func get_active_opportunity() -> OpportunityStateRecord:
|
||||||
@@ -820,18 +810,26 @@ func get_active_opportunity_helper() -> OpportunityHelperResult:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_opportunity_player_response() -> OpportunityPlayerResponseResult:
|
||||||
|
var active := get_active_opportunity()
|
||||||
|
if active == null:
|
||||||
|
return null
|
||||||
|
var helper := get_active_opportunity_helper()
|
||||||
|
return opportunity_system.find_player_response(
|
||||||
|
active, economy.get_storage(active.get_target_id()), resource_states, helper != null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func get_latest_opportunity_for_npc(npc_id: int) -> OpportunityStateRecord:
|
func get_latest_opportunity_for_npc(npc_id: int) -> OpportunityStateRecord:
|
||||||
return opportunity_system.get_latest_for_npc(npc_id)
|
return opportunity_system.get_latest_for_npc(npc_id)
|
||||||
|
|
||||||
|
|
||||||
func get_opportunity_trigger_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
func get_opportunity_trigger_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
||||||
var event_id := opportunity.get_trigger_event_id() if opportunity != null else -1
|
return event_log.get_by_id(opportunity.get_trigger_event_id() if opportunity != null else -1)
|
||||||
return event_log.get_by_id(event_id)
|
|
||||||
|
|
||||||
|
|
||||||
func get_opportunity_resolution_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
func get_opportunity_resolution_event(opportunity: OpportunityStateRecord) -> EconomicEventRecord:
|
||||||
var event_id := opportunity.get_resolution_event_id() if opportunity != null else -1
|
return event_log.get_by_id(opportunity.get_resolution_event_id() if opportunity != null else -1)
|
||||||
return event_log.get_by_id(event_id)
|
|
||||||
|
|
||||||
|
|
||||||
func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
||||||
|
|||||||
@@ -91,6 +91,15 @@ func get_by_id(event_id: int) -> EconomicEventRecord:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_by_ids(event_ids: Array[int]) -> Array[EconomicEventRecord]:
|
||||||
|
var results: Array[EconomicEventRecord] = []
|
||||||
|
for event_id in event_ids:
|
||||||
|
var event := get_by_id(event_id)
|
||||||
|
if event != null:
|
||||||
|
results.append(event)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
func get_consumption_rates(
|
func get_consumption_rates(
|
||||||
current_tick: int,
|
current_tick: int,
|
||||||
window_ticks: int = DEFAULT_RATE_WINDOW_TICKS,
|
window_ticks: int = DEFAULT_RATE_WINDOW_TICKS,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
class_name OpportunityPlayerResponseResult
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var opportunity_id: int
|
||||||
|
var trigger_event_id: int
|
||||||
|
var action_id: StringName
|
||||||
|
var resource_id: StringName
|
||||||
|
var target_id: StringName
|
||||||
|
var available_source_count: int
|
||||||
|
var reason: String
|
||||||
|
|
||||||
|
|
||||||
|
func _init(result: Dictionary) -> void:
|
||||||
|
opportunity_id = int(result["opportunity_id"])
|
||||||
|
trigger_event_id = int(result["trigger_event_id"])
|
||||||
|
action_id = StringName(result["action_id"])
|
||||||
|
resource_id = StringName(result["resource_id"])
|
||||||
|
target_id = StringName(result["target_id"])
|
||||||
|
available_source_count = int(result["available_source_count"])
|
||||||
|
reason = String(result["reason"])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://2nyepaa3b77v
|
||||||
@@ -193,6 +193,62 @@ func find_capable_helper(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func find_player_response(
|
||||||
|
opportunity: OpportunityStateRecord,
|
||||||
|
target_storage: StorageStateRecord,
|
||||||
|
resource_states: Dictionary,
|
||||||
|
has_capable_helper: bool
|
||||||
|
) -> OpportunityPlayerResponseResult:
|
||||||
|
if (
|
||||||
|
opportunity == null
|
||||||
|
or not opportunity.is_open()
|
||||||
|
or target_storage == null
|
||||||
|
or target_storage.get_storage_id() != opportunity.get_target_id()
|
||||||
|
or has_capable_helper
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
var remaining := maxf(
|
||||||
|
opportunity.get_target_amount() - target_storage.get_amount(opportunity.get_resource_id()),
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
if remaining <= 0.0 or target_storage.get_available_capacity() < remaining:
|
||||||
|
return null
|
||||||
|
var action_ids := _get_supply_action_ids(opportunity.get_resource_id())
|
||||||
|
if action_ids.is_empty():
|
||||||
|
return null
|
||||||
|
var gather_definition := SimulationDefinitions.get_action(action_ids["gather"])
|
||||||
|
if gather_definition == null:
|
||||||
|
return null
|
||||||
|
var available_source_count := _count_player_resource_sources(
|
||||||
|
remaining, opportunity.get_resource_id(), gather_definition, resource_states
|
||||||
|
)
|
||||||
|
if available_source_count == 0:
|
||||||
|
return null
|
||||||
|
return (
|
||||||
|
OpportunityPlayerResponseResult
|
||||||
|
. new(
|
||||||
|
{
|
||||||
|
"opportunity_id": opportunity.get_opportunity_id(),
|
||||||
|
"trigger_event_id": opportunity.get_trigger_event_id(),
|
||||||
|
"action_id": gather_definition.action_id,
|
||||||
|
"resource_id": opportunity.get_resource_id(),
|
||||||
|
"target_id": target_storage.get_storage_id(),
|
||||||
|
"available_source_count": available_source_count,
|
||||||
|
"reason":
|
||||||
|
(
|
||||||
|
"No capable helper; %d player-usable finite %s source%s can supply %s"
|
||||||
|
% [
|
||||||
|
available_source_count,
|
||||||
|
String(opportunity.get_resource_id()).capitalize(),
|
||||||
|
"" if available_source_count == 1 else "s",
|
||||||
|
String(target_storage.get_storage_id()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _build_helper_candidate(
|
func _build_helper_candidate(
|
||||||
npc: SimNPC,
|
npc: SimNPC,
|
||||||
opportunity: OpportunityStateRecord,
|
opportunity: OpportunityStateRecord,
|
||||||
@@ -299,6 +355,29 @@ static func _count_available_resource_sources(
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
static func _count_player_resource_sources(
|
||||||
|
remaining: float,
|
||||||
|
resource_id: StringName,
|
||||||
|
gather_definition: ActionDefinition,
|
||||||
|
resource_states: Dictionary
|
||||||
|
) -> int:
|
||||||
|
var count := 0
|
||||||
|
for value in resource_states.values():
|
||||||
|
var state := value as ResourceStateRecord
|
||||||
|
if (
|
||||||
|
state == null
|
||||||
|
or state.get_action_id() != gather_definition.resource_action_id
|
||||||
|
or state.get_resource_id() != resource_id
|
||||||
|
or not state.can_player_use_resource()
|
||||||
|
or not state.can_extract()
|
||||||
|
or state.get_yield_per_action() <= 0.0
|
||||||
|
or state.get_amount_remaining() < remaining
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
static func _sort_helper_candidates(first: Dictionary, second: Dictionary) -> bool:
|
static func _sort_helper_candidates(first: Dictionary, second: Dictionary) -> bool:
|
||||||
if first["uses_inventory"] != second["uses_inventory"]:
|
if first["uses_inventory"] != second["uses_inventory"]:
|
||||||
return bool(first["uses_inventory"])
|
return bool(first["uses_inventory"])
|
||||||
|
|||||||
@@ -311,16 +311,25 @@ func _run() -> void:
|
|||||||
runtime_opportunity.get_trigger_event_id() if runtime_opportunity != null else -1
|
runtime_opportunity.get_trigger_event_id() if runtime_opportunity != null else -1
|
||||||
)
|
)
|
||||||
var runtime_helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
var runtime_helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
||||||
|
var runtime_player_response: OpportunityPlayerResponseResult = (
|
||||||
|
simulation_manager.get_active_opportunity_player_response()
|
||||||
|
)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
runtime_opportunity != null
|
runtime_opportunity != null
|
||||||
and runtime_helper == null
|
and runtime_helper == null
|
||||||
|
and runtime_player_response != null
|
||||||
|
and runtime_player_response.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||||
|
and runtime_player_response.target_id == SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||||
|
and runtime_player_response.available_source_count > 0
|
||||||
and not simulation_manager.npc_knows_event(witness.id, runtime_trigger_id)
|
and not simulation_manager.npc_knows_event(witness.id, runtime_trigger_id)
|
||||||
and whisper_kicker.text == "VILLAGE NEED"
|
and whisper_kicker.text == "YOU CAN HELP"
|
||||||
and contributor.npc_name in whisper_message.text
|
and (
|
||||||
and "needs wood" in whisper_message.text
|
"Harvest a tree; wood goes straight to the village woodpile."
|
||||||
|
in whisper_message.text
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"The real blocked-work need should surface while the distant helper remains uninformed"
|
"An unassisted blocked-work need should surface its real player harvest route"
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
@@ -330,6 +339,7 @@ func _run() -> void:
|
|||||||
"Possible helper: none informed, trusted, and able to supply"
|
"Possible helper: none informed, trusted, and able to supply"
|
||||||
in village_stats_label.text
|
in village_stats_label.text
|
||||||
)
|
)
|
||||||
|
and "Player route: Gather Wood → Village Woodpile" in village_stats_label.text
|
||||||
and "◆ Find wood for Study" in inspector_label.text
|
and "◆ Find wood for Study" in inspector_label.text
|
||||||
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
and not contributor_visual.get_node("OpportunityConcernRoot").visible
|
||||||
),
|
),
|
||||||
@@ -348,6 +358,7 @@ func _run() -> void:
|
|||||||
witness.id, runtime_trigger_id
|
witness.id, runtime_trigger_id
|
||||||
)
|
)
|
||||||
runtime_helper = simulation_manager.get_active_opportunity_helper()
|
runtime_helper = simulation_manager.get_active_opportunity_helper()
|
||||||
|
runtime_player_response = simulation_manager.get_active_opportunity_player_response()
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
communicated_trigger != null
|
communicated_trigger != null
|
||||||
@@ -357,6 +368,7 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
and communicated_trigger.get_source_npc_id() == contributor.id
|
and communicated_trigger.get_source_npc_id() == contributor.id
|
||||||
and runtime_helper != null
|
and runtime_helper != null
|
||||||
|
and runtime_player_response == null
|
||||||
and runtime_helper.helper_npc_id == witness.id
|
and runtime_helper.helper_npc_id == witness.id
|
||||||
and runtime_helper.action_id == SimulationIds.ACTION_GATHER_WOOD
|
and runtime_helper.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||||
and whisper_kicker.text == "NEWS TRAVELS"
|
and whisper_kicker.text == "NEWS TRAVELS"
|
||||||
|
|||||||
@@ -306,6 +306,60 @@ func test_ready_inventory_then_trust_and_stable_id_rank_helpers_deterministicall
|
|||||||
assert_eq(result.helper_npc_id, lower_id_carrier.id)
|
assert_eq(result.helper_npc_id, lower_id_carrier.id)
|
||||||
|
|
||||||
|
|
||||||
|
func test_player_response_requires_no_helper_and_a_real_player_supply_route() -> void:
|
||||||
|
var fixture := _open_fixture()
|
||||||
|
var system: VillageOpportunitySystem = fixture["system"]
|
||||||
|
var pantry: StorageStateRecord = fixture["pantry"]
|
||||||
|
var opportunity: OpportunityStateRecord = system.get_open_opportunity()
|
||||||
|
var player_source := _resource_state(
|
||||||
|
&"player_berries", SimulationIds.ACTION_GATHER_FOOD, SimulationIds.RESOURCE_FOOD, 1.0
|
||||||
|
)
|
||||||
|
var npc_only_source := _resource_state(
|
||||||
|
&"npc_berries",
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
|
SimulationIds.RESOURCE_FOOD,
|
||||||
|
2.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
var resource_states := {
|
||||||
|
player_source.get_node_id(): player_source,
|
||||||
|
npc_only_source.get_node_id(): npc_only_source,
|
||||||
|
}
|
||||||
|
assert_true(player_source.reserve(99))
|
||||||
|
var opportunity_before := opportunity.to_dictionary()
|
||||||
|
var pantry_before := pantry.to_dictionary()
|
||||||
|
var source_before := player_source.to_dictionary()
|
||||||
|
var response: OpportunityPlayerResponseResult = system.find_player_response(
|
||||||
|
opportunity, pantry, resource_states, false
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_not_null(response)
|
||||||
|
assert_eq(response.opportunity_id, opportunity.get_opportunity_id())
|
||||||
|
assert_eq(response.trigger_event_id, opportunity.get_trigger_event_id())
|
||||||
|
assert_eq(response.action_id, SimulationIds.ACTION_GATHER_FOOD)
|
||||||
|
assert_eq(response.resource_id, SimulationIds.RESOURCE_FOOD)
|
||||||
|
assert_eq(response.target_id, SimulationIds.STORAGE_VILLAGE_PANTRY)
|
||||||
|
assert_eq(response.available_source_count, 1)
|
||||||
|
assert_string_contains(response.reason, "No capable helper")
|
||||||
|
assert_string_contains(response.reason, "player-usable finite Food source")
|
||||||
|
assert_eq(opportunity.to_dictionary(), opportunity_before)
|
||||||
|
assert_eq(pantry.to_dictionary(), pantry_before)
|
||||||
|
assert_eq(player_source.to_dictionary(), source_before)
|
||||||
|
assert_null(system.find_player_response(opportunity, pantry, resource_states, true))
|
||||||
|
|
||||||
|
player_source.set_enabled(false)
|
||||||
|
assert_null(system.find_player_response(opportunity, pantry, resource_states, false))
|
||||||
|
var full_pantry := StorageStateRecord.create(
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY, {String(SimulationIds.RESOURCE_WOOD): 1.0}, 1.0
|
||||||
|
)
|
||||||
|
player_source.set_enabled(true)
|
||||||
|
assert_null(system.find_player_response(opportunity, full_pantry, resource_states, false))
|
||||||
|
var wrong_target := StorageStateRecord.create(&"not_the_pantry", {}, 1.0)
|
||||||
|
assert_null(system.find_player_response(opportunity, wrong_target, resource_states, false))
|
||||||
|
|
||||||
|
|
||||||
func test_wood_helper_uses_available_finite_source_and_respects_reservations() -> void:
|
func test_wood_helper_uses_available_finite_source_and_respects_reservations() -> void:
|
||||||
var system := VillageOpportunitySystemScript.new()
|
var system := VillageOpportunitySystemScript.new()
|
||||||
var interested := _npc(0, 20.0)
|
var interested := _npc(0, 20.0)
|
||||||
@@ -483,7 +537,9 @@ func _resource_state(
|
|||||||
resource_id: StringName,
|
resource_id: StringName,
|
||||||
amount: float,
|
amount: float,
|
||||||
discovery_priority: float = 0.0,
|
discovery_priority: float = 0.0,
|
||||||
safety_risk: float = 0.0
|
safety_risk: float = 0.0,
|
||||||
|
can_player_use: bool = true,
|
||||||
|
can_npcs_use: bool = true
|
||||||
) -> ResourceStateRecord:
|
) -> ResourceStateRecord:
|
||||||
var node := ResourceNode.new()
|
var node := ResourceNode.new()
|
||||||
node.node_id = node_id
|
node.node_id = node_id
|
||||||
@@ -493,6 +549,8 @@ func _resource_state(
|
|||||||
node.yield_per_action = 1.0
|
node.yield_per_action = 1.0
|
||||||
node.discovery_priority = discovery_priority
|
node.discovery_priority = discovery_priority
|
||||||
node.safety_risk = safety_risk
|
node.safety_risk = safety_risk
|
||||||
|
node.can_player_use = can_player_use
|
||||||
|
node.can_npcs_use = can_npcs_use
|
||||||
var state := ResourceStateRecord.create_from_node(node)
|
var state := ResourceStateRecord.create_from_node(node)
|
||||||
node.free()
|
node.free()
|
||||||
return state
|
return state
|
||||||
|
|||||||
@@ -202,6 +202,48 @@ func _run() -> void:
|
|||||||
root.add_child(tree)
|
root.add_child(tree)
|
||||||
await process_frame
|
await process_frame
|
||||||
_check(player_manager.register_resource_node(tree), "Player branch should bind a real tree")
|
_check(player_manager.register_resource_node(tree), "Player branch should bind a real tree")
|
||||||
|
var checksum_before_player_query: String = player_manager.get_state_checksum()
|
||||||
|
var player_response: OpportunityPlayerResponseResult = (
|
||||||
|
player_manager.get_active_opportunity_player_response()
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
player_manager.get_active_opportunity_helper() == null
|
||||||
|
and player_response != null
|
||||||
|
and player_response.action_id == SimulationIds.ACTION_GATHER_WOOD
|
||||||
|
and player_response.resource_id == SimulationIds.RESOURCE_WOOD
|
||||||
|
and player_response.target_id == SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||||
|
and player_response.available_source_count == 1
|
||||||
|
),
|
||||||
|
"An unassisted wood need should derive one ordinary player tree-to-woodpile route"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
player_manager.get_state_checksum() == checksum_before_player_query,
|
||||||
|
"Player-response discovery should not mutate authoritative simulation state"
|
||||||
|
)
|
||||||
|
var player_active_json: String = player_manager.serialize_state()
|
||||||
|
var player_active_restored := _create_manager(1210)
|
||||||
|
_check(
|
||||||
|
player_active_restored.restore_state_from_json(player_active_json),
|
||||||
|
"An active player-response route should restore through its existing facts"
|
||||||
|
)
|
||||||
|
var restored_player_response: OpportunityPlayerResponseResult = (
|
||||||
|
player_active_restored.get_active_opportunity_player_response()
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
player_active_restored.get_state_checksum() == checksum_before_player_query
|
||||||
|
and restored_player_response != null
|
||||||
|
and restored_player_response.action_id == player_response.action_id
|
||||||
|
and restored_player_response.target_id == player_response.target_id
|
||||||
|
and (
|
||||||
|
restored_player_response.available_source_count
|
||||||
|
== player_response.available_source_count
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Restore should re-derive the same player response without persisted hint state"
|
||||||
|
)
|
||||||
|
player_active_restored.free()
|
||||||
_check(
|
_check(
|
||||||
is_equal_approx(player_manager.harvest_resource_node(tree), 1.0),
|
is_equal_approx(player_manager.harvest_resource_node(tree), 1.0),
|
||||||
"Player tree extraction should supply the authoritative woodpile"
|
"Player tree extraction should supply the authoritative woodpile"
|
||||||
@@ -227,6 +269,10 @@ func _run() -> void:
|
|||||||
),
|
),
|
||||||
"Player extraction should resolve through its exact existing economy event"
|
"Player extraction should resolve through its exact existing economy event"
|
||||||
)
|
)
|
||||||
|
_check(
|
||||||
|
player_manager.get_active_opportunity_player_response() == null,
|
||||||
|
"Resolving the need should remove the derived player response"
|
||||||
|
)
|
||||||
tree.queue_free()
|
tree.queue_free()
|
||||||
player_manager.free()
|
player_manager.free()
|
||||||
|
|
||||||
@@ -347,6 +393,7 @@ func _create_player_tree() -> ResourceNode:
|
|||||||
tree.resource_id = SimulationIds.RESOURCE_WOOD
|
tree.resource_id = SimulationIds.RESOURCE_WOOD
|
||||||
tree.initial_amount = 1.0
|
tree.initial_amount = 1.0
|
||||||
tree.yield_per_action = 1.0
|
tree.yield_per_action = 1.0
|
||||||
|
tree.can_npcs_use = false
|
||||||
tree.can_player_use = true
|
tree.can_player_use = true
|
||||||
tree.debug_label_enabled = false
|
tree.debug_label_enabled = false
|
||||||
var interaction_point := Marker3D.new()
|
var interaction_point := Marker3D.new()
|
||||||
@@ -359,6 +406,7 @@ func _create_helper_tree() -> ResourceNode:
|
|||||||
var tree := _create_player_tree()
|
var tree := _create_player_tree()
|
||||||
tree.name = "HelperTree"
|
tree.name = "HelperTree"
|
||||||
tree.node_id = &"helper_tree"
|
tree.node_id = &"helper_tree"
|
||||||
|
tree.can_npcs_use = true
|
||||||
tree.can_player_use = false
|
tree.can_player_use = false
|
||||||
tree.discovery_priority = 4.0
|
tree.discovery_priority = 4.0
|
||||||
return tree
|
return tree
|
||||||
|
|||||||
+22
-1
@@ -291,13 +291,34 @@ func _build_opportunity_helper_display() -> String:
|
|||||||
return ""
|
return ""
|
||||||
var helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
var helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
|
||||||
if helper == null:
|
if helper == null:
|
||||||
return "\nPossible helper: none informed, trusted, and able to supply"
|
var player_text := _build_opportunity_player_response_display()
|
||||||
|
return "\nPossible helper: none informed, trusted, and able to supply" + player_text
|
||||||
return (
|
return (
|
||||||
"\nPossible helper: %s — %s\n%s"
|
"\nPossible helper: %s — %s\n%s"
|
||||||
% [_get_npc_name(helper.helper_npc_id), _get_action_name(helper.action_id), helper.reason]
|
% [_get_npc_name(helper.helper_npc_id), _get_action_name(helper.action_id), helper.reason]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_opportunity_player_response_display() -> String:
|
||||||
|
if not simulation_manager.has_method("get_active_opportunity_player_response"):
|
||||||
|
return ""
|
||||||
|
var response: OpportunityPlayerResponseResult = (
|
||||||
|
simulation_manager.get_active_opportunity_player_response()
|
||||||
|
)
|
||||||
|
if response == null:
|
||||||
|
return ""
|
||||||
|
var source_suffix := "" if response.available_source_count == 1 else "s"
|
||||||
|
return (
|
||||||
|
"\nPlayer route: %s → %s · %d finite source%s"
|
||||||
|
% [
|
||||||
|
_get_action_name(response.action_id),
|
||||||
|
String(response.target_id).replace("_", " ").capitalize(),
|
||||||
|
response.available_source_count,
|
||||||
|
source_suffix,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _build_npc_opportunity_display(npc: SimNPC) -> String:
|
func _build_npc_opportunity_display(npc: SimNPC) -> String:
|
||||||
if not simulation_manager.has_method("get_latest_opportunity_for_npc"):
|
if not simulation_manager.has_method("get_latest_opportunity_for_npc"):
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ extends Control
|
|||||||
|
|
||||||
const DISPLAY_SECONDS := 4.2
|
const DISPLAY_SECONDS := 4.2
|
||||||
const ENTER_OFFSET := 8.0
|
const ENTER_OFFSET := 8.0
|
||||||
|
const FEEDBACK_PLAYER_RESPONSE := &"player_response"
|
||||||
|
|
||||||
@export var simulation_manager: Node
|
@export var simulation_manager: Node
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ var resting_position: Vector2
|
|||||||
var feedback_version := 0
|
var feedback_version := 0
|
||||||
var active_trigger_event_id := OpportunityStateRecord.NO_EVENT_ID
|
var active_trigger_event_id := OpportunityStateRecord.NO_EVENT_ID
|
||||||
var informed_listener_id := -1
|
var informed_listener_id := -1
|
||||||
|
var feedback_kind := &""
|
||||||
|
var player_response_available := false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -28,12 +31,29 @@ func _ready() -> void:
|
|||||||
simulation_manager.opportunity_invalidated.connect(_on_opportunity_invalidated)
|
simulation_manager.opportunity_invalidated.connect(_on_opportunity_invalidated)
|
||||||
simulation_manager.event_knowledge_transferred.connect(_on_event_knowledge_transferred)
|
simulation_manager.event_knowledge_transferred.connect(_on_event_knowledge_transferred)
|
||||||
simulation_manager.npc_decision_recorded.connect(_on_npc_decision_recorded)
|
simulation_manager.npc_decision_recorded.connect(_on_npc_decision_recorded)
|
||||||
|
simulation_manager.event_knowledge_changed.connect(_on_helper_facts_changed)
|
||||||
|
simulation_manager.event_knowledge_forgotten.connect(_on_helper_fact_forgotten)
|
||||||
|
simulation_manager.relationship_changed.connect(_on_helper_relationship_changed)
|
||||||
|
simulation_manager.npc_inventory_changed.connect(_on_helper_inventory_changed)
|
||||||
|
simulation_manager.village_changed.connect(_on_village_changed)
|
||||||
simulation_manager.state_restored.connect(_on_state_restored)
|
simulation_manager.state_restored.connect(_on_state_restored)
|
||||||
|
player_response_available = simulation_manager.get_active_opportunity_player_response() != null
|
||||||
|
|
||||||
|
|
||||||
func _on_opportunity_opened(opportunity: OpportunityStateRecord) -> void:
|
func _on_opportunity_opened(opportunity: OpportunityStateRecord) -> void:
|
||||||
active_trigger_event_id = opportunity.get_trigger_event_id()
|
active_trigger_event_id = opportunity.get_trigger_event_id()
|
||||||
informed_listener_id = -1
|
informed_listener_id = -1
|
||||||
|
var player_response: OpportunityPlayerResponseResult = (
|
||||||
|
simulation_manager.get_active_opportunity_player_response()
|
||||||
|
)
|
||||||
|
player_response_available = player_response != null
|
||||||
|
if player_response != null:
|
||||||
|
_show_feedback(
|
||||||
|
"You can help",
|
||||||
|
_build_player_response_message(player_response),
|
||||||
|
FEEDBACK_PLAYER_RESPONSE
|
||||||
|
)
|
||||||
|
return
|
||||||
_show_feedback("Village need", _build_need_message(opportunity))
|
_show_feedback("Village need", _build_need_message(opportunity))
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +98,44 @@ func _on_npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_helper_facts_changed(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||||
|
_refresh_player_response()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_helper_fact_forgotten(_knower_id: int, _event: EconomicEventRecord) -> void:
|
||||||
|
_refresh_player_response()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_helper_relationship_changed(
|
||||||
|
_relationship: RelationshipStateRecord, _cause_event: EconomicEventRecord
|
||||||
|
) -> void:
|
||||||
|
_refresh_player_response()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_helper_inventory_changed(_npc: SimNPC, _item_id: StringName, _amount: float) -> void:
|
||||||
|
_refresh_player_response()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_village_changed(_village: SimVillage) -> void:
|
||||||
|
_refresh_player_response()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_player_response() -> void:
|
||||||
|
var response: OpportunityPlayerResponseResult = (
|
||||||
|
simulation_manager.get_active_opportunity_player_response()
|
||||||
|
)
|
||||||
|
var is_available := response != null
|
||||||
|
if is_available == player_response_available:
|
||||||
|
return
|
||||||
|
player_response_available = is_available
|
||||||
|
if response != null:
|
||||||
|
_show_feedback(
|
||||||
|
"You can help", _build_player_response_message(response), FEEDBACK_PLAYER_RESPONSE
|
||||||
|
)
|
||||||
|
elif feedback_kind == FEEDBACK_PLAYER_RESPONSE:
|
||||||
|
_reset_feedback()
|
||||||
|
|
||||||
|
|
||||||
func _on_opportunity_resolved(
|
func _on_opportunity_resolved(
|
||||||
opportunity: OpportunityStateRecord, resolution_event: EconomicEventRecord
|
opportunity: OpportunityStateRecord, resolution_event: EconomicEventRecord
|
||||||
) -> void:
|
) -> void:
|
||||||
@@ -92,6 +150,7 @@ func _on_opportunity_resolved(
|
|||||||
)
|
)
|
||||||
_show_feedback("Need met", "%s %s." % [actor_name, outcome])
|
_show_feedback("Need met", "%s %s." % [actor_name, outcome])
|
||||||
informed_listener_id = -1
|
informed_listener_id = -1
|
||||||
|
player_response_available = false
|
||||||
|
|
||||||
|
|
||||||
func _on_opportunity_invalidated(opportunity: OpportunityStateRecord) -> void:
|
func _on_opportunity_invalidated(opportunity: OpportunityStateRecord) -> void:
|
||||||
@@ -99,6 +158,7 @@ func _on_opportunity_invalidated(opportunity: OpportunityStateRecord) -> void:
|
|||||||
return
|
return
|
||||||
_show_feedback("Need closed", "The village could no longer act on this report.")
|
_show_feedback("Need closed", "The village could no longer act on this report.")
|
||||||
informed_listener_id = -1
|
informed_listener_id = -1
|
||||||
|
player_response_available = false
|
||||||
|
|
||||||
|
|
||||||
func _on_state_restored() -> void:
|
func _on_state_restored() -> void:
|
||||||
@@ -110,6 +170,7 @@ func _on_state_restored() -> void:
|
|||||||
else OpportunityStateRecord.NO_EVENT_ID
|
else OpportunityStateRecord.NO_EVENT_ID
|
||||||
)
|
)
|
||||||
informed_listener_id = -1
|
informed_listener_id = -1
|
||||||
|
player_response_available = simulation_manager.get_active_opportunity_player_response() != null
|
||||||
|
|
||||||
|
|
||||||
func _build_need_message(opportunity: OpportunityStateRecord) -> String:
|
func _build_need_message(opportunity: OpportunityStateRecord) -> String:
|
||||||
@@ -123,6 +184,22 @@ func _build_need_message(opportunity: OpportunityStateRecord) -> String:
|
|||||||
return "%s needs wood to finish %s." % [interested_name, action_name]
|
return "%s needs wood to finish %s." % [interested_name, action_name]
|
||||||
|
|
||||||
|
|
||||||
|
func _build_player_response_message(response: OpportunityPlayerResponseResult) -> String:
|
||||||
|
var destination := _get_destination_name(response.target_id)
|
||||||
|
if response.resource_id == SimulationIds.RESOURCE_FOOD:
|
||||||
|
return "Forage food; it goes straight to the %s." % destination
|
||||||
|
return "Harvest a tree; wood goes straight to the %s." % destination
|
||||||
|
|
||||||
|
|
||||||
|
func _get_destination_name(target_id: StringName) -> String:
|
||||||
|
match target_id:
|
||||||
|
SimulationIds.STORAGE_VILLAGE_PANTRY:
|
||||||
|
return "village pantry"
|
||||||
|
SimulationIds.STORAGE_VILLAGE_WOODPILE:
|
||||||
|
return "village woodpile"
|
||||||
|
return String(target_id).replace("_", " ")
|
||||||
|
|
||||||
|
|
||||||
func _get_need_name(opportunity: OpportunityStateRecord) -> String:
|
func _get_need_name(opportunity: OpportunityStateRecord) -> String:
|
||||||
return (
|
return (
|
||||||
"empty-pantry worry"
|
"empty-pantry worry"
|
||||||
@@ -143,13 +220,14 @@ func _get_npc_name(npc_id: int) -> String:
|
|||||||
return "Someone"
|
return "Someone"
|
||||||
|
|
||||||
|
|
||||||
func _show_feedback(heading: String, body: String) -> void:
|
func _show_feedback(heading: String, body: String, kind: StringName = &"") -> void:
|
||||||
feedback_version += 1
|
feedback_version += 1
|
||||||
var version := feedback_version
|
var version := feedback_version
|
||||||
if feedback_tween != null:
|
if feedback_tween != null:
|
||||||
feedback_tween.kill()
|
feedback_tween.kill()
|
||||||
kicker.text = heading.to_upper()
|
kicker.text = heading.to_upper()
|
||||||
message.text = body
|
message.text = body
|
||||||
|
feedback_kind = kind
|
||||||
visible = true
|
visible = true
|
||||||
position = resting_position + Vector2(0.0, ENTER_OFFSET)
|
position = resting_position + Vector2(0.0, ENTER_OFFSET)
|
||||||
modulate.a = 0.0
|
modulate.a = 0.0
|
||||||
@@ -174,6 +252,7 @@ func _fade_after_delay(version: int) -> void:
|
|||||||
await feedback_tween.finished
|
await feedback_tween.finished
|
||||||
if version == feedback_version:
|
if version == feedback_version:
|
||||||
visible = false
|
visible = false
|
||||||
|
feedback_kind = &""
|
||||||
|
|
||||||
|
|
||||||
func _reset_feedback() -> void:
|
func _reset_feedback() -> void:
|
||||||
@@ -181,6 +260,7 @@ func _reset_feedback() -> void:
|
|||||||
if feedback_tween != null:
|
if feedback_tween != null:
|
||||||
feedback_tween.kill()
|
feedback_tween.kill()
|
||||||
visible = false
|
visible = false
|
||||||
|
feedback_kind = &""
|
||||||
position = resting_position
|
position = resting_position
|
||||||
modulate.a = 0.0
|
modulate.a = 0.0
|
||||||
if accent != null:
|
if accent != null:
|
||||||
|
|||||||
Reference in New Issue
Block a user