feat: let the player talk to villagers and accept or decline personal requests
This commit is contained in:
@@ -5,6 +5,7 @@ const KIND_ANIMAL := &"animal"
|
|||||||
const KIND_RESOURCE := &"resource"
|
const KIND_RESOURCE := &"resource"
|
||||||
const KIND_PANTRY := &"pantry"
|
const KIND_PANTRY := &"pantry"
|
||||||
const KIND_DEPOSIT := &"deposit"
|
const KIND_DEPOSIT := &"deposit"
|
||||||
|
const KIND_TALK := &"talk"
|
||||||
const KIND_GUARD := &"guard"
|
const KIND_GUARD := &"guard"
|
||||||
const KIND_STUDY := &"study"
|
const KIND_STUDY := &"study"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
class_name PlayerTalkResult
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var npc_id: int
|
||||||
|
var npc_name: String
|
||||||
|
var greeting: String
|
||||||
|
var has_need: bool
|
||||||
|
var need_text: String
|
||||||
|
var quest_available: bool
|
||||||
|
var quest_reward: float
|
||||||
|
var can_accept: bool
|
||||||
|
var can_decline: bool
|
||||||
|
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
talk_npc_id: int,
|
||||||
|
talk_npc_name: String,
|
||||||
|
talk_greeting: String,
|
||||||
|
talk_has_need: bool,
|
||||||
|
talk_need_text: String,
|
||||||
|
talk_quest_available: bool,
|
||||||
|
talk_quest_reward: float,
|
||||||
|
talk_can_accept: bool,
|
||||||
|
talk_can_decline: bool
|
||||||
|
) -> void:
|
||||||
|
npc_id = talk_npc_id
|
||||||
|
npc_name = talk_npc_name
|
||||||
|
greeting = talk_greeting
|
||||||
|
has_need = talk_has_need
|
||||||
|
need_text = talk_need_text
|
||||||
|
quest_available = talk_quest_available
|
||||||
|
quest_reward = talk_quest_reward
|
||||||
|
can_accept = talk_can_accept
|
||||||
|
can_decline = talk_can_decline
|
||||||
|
|
||||||
|
|
||||||
|
func cache_key() -> String:
|
||||||
|
return (
|
||||||
|
"|"
|
||||||
|
. join(
|
||||||
|
[
|
||||||
|
str(npc_id),
|
||||||
|
npc_name,
|
||||||
|
greeting,
|
||||||
|
str(has_need),
|
||||||
|
need_text,
|
||||||
|
str(quest_available),
|
||||||
|
"%.2f" % quest_reward,
|
||||||
|
str(can_accept),
|
||||||
|
str(can_decline),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://i7pjb5605cgr
|
||||||
@@ -72,6 +72,8 @@ func try_interact() -> void:
|
|||||||
_execute_resource_interaction(context)
|
_execute_resource_interaction(context)
|
||||||
PlayerInteractionResult.KIND_DEPOSIT:
|
PlayerInteractionResult.KIND_DEPOSIT:
|
||||||
_execute_deposit_interaction(context)
|
_execute_deposit_interaction(context)
|
||||||
|
PlayerInteractionResult.KIND_TALK:
|
||||||
|
_execute_talk_interaction(context)
|
||||||
PlayerInteractionResult.KIND_GUARD:
|
PlayerInteractionResult.KIND_GUARD:
|
||||||
simulation_manager.add_safety(3.0)
|
simulation_manager.add_safety(3.0)
|
||||||
interaction_feedback.emit(
|
interaction_feedback.emit(
|
||||||
@@ -131,9 +133,53 @@ func get_interaction_context() -> PlayerInteractionResult:
|
|||||||
)
|
)
|
||||||
if is_near_storage(pantry_storage):
|
if is_near_storage(pantry_storage):
|
||||||
return _build_pantry_context()
|
return _build_pantry_context()
|
||||||
|
var talk_npc := _find_talk_villager()
|
||||||
|
if talk_npc != null:
|
||||||
|
return _build_talk_context(talk_npc)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _find_talk_villager() -> SimNPC:
|
||||||
|
if (
|
||||||
|
simulation_manager == null
|
||||||
|
or world_view_manager == null
|
||||||
|
or not world_view_manager.has_method("find_nearest_active_npc_id")
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
var npc_id: int = world_view_manager.find_nearest_active_npc_id(
|
||||||
|
global_position, interaction_range
|
||||||
|
)
|
||||||
|
if npc_id < 0:
|
||||||
|
return null
|
||||||
|
var npc := _find_npc(npc_id)
|
||||||
|
if npc == null or npc.is_dead:
|
||||||
|
return null
|
||||||
|
return npc
|
||||||
|
|
||||||
|
|
||||||
|
func _build_talk_context(npc: SimNPC) -> PlayerInteractionResult:
|
||||||
|
var talk: PlayerTalkResult = simulation_manager.get_villager_talk(npc.id)
|
||||||
|
if talk == null or not talk.has_need:
|
||||||
|
return null
|
||||||
|
var prompt := "Talk to %s" % npc.npc_name
|
||||||
|
if talk.quest_available:
|
||||||
|
prompt = "Accept %s's request" % npc.npc_name
|
||||||
|
elif talk.can_accept:
|
||||||
|
prompt = "Offer to help %s" % npc.npc_name
|
||||||
|
var detail := talk.need_text
|
||||||
|
if talk.quest_available:
|
||||||
|
detail = "Help +%.0f Standing" % talk.quest_reward
|
||||||
|
return PlayerInteractionResult.new(
|
||||||
|
PlayerInteractionResult.KIND_TALK,
|
||||||
|
SimulationIds.ACTION_IDLE,
|
||||||
|
SimulationIds.npc_inventory_id(npc.id),
|
||||||
|
npc.npc_name,
|
||||||
|
prompt,
|
||||||
|
detail,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func get_nearby_villager_inspection() -> VillagerInspectionResult:
|
func get_nearby_villager_inspection() -> VillagerInspectionResult:
|
||||||
if (
|
if (
|
||||||
simulation_manager == null
|
simulation_manager == null
|
||||||
@@ -437,6 +483,38 @@ func _execute_deposit_interaction(context: PlayerInteractionResult) -> void:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _execute_talk_interaction(context: PlayerInteractionResult) -> void:
|
||||||
|
var npc_id := _talk_npc_id_from_target(context.target_id)
|
||||||
|
if npc_id < 0:
|
||||||
|
return
|
||||||
|
var quest: PlayerQuestRecord = simulation_manager.accept_villager_request(npc_id)
|
||||||
|
if quest == null:
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"%s declines" % context.display_name,
|
||||||
|
"They have no request you can accept right now.",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return
|
||||||
|
interaction_feedback.emit(
|
||||||
|
"Request accepted",
|
||||||
|
(
|
||||||
|
"%s asked you for help · +%.0f Standing on completion"
|
||||||
|
% [context.display_name, quest.get_standing_reward()]
|
||||||
|
),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _talk_npc_id_from_target(target_id: StringName) -> int:
|
||||||
|
var prefix := "npc_"
|
||||||
|
var suffix := "_inventory"
|
||||||
|
var raw := String(target_id)
|
||||||
|
if not raw.begins_with(prefix) or not raw.ends_with(suffix):
|
||||||
|
return -1
|
||||||
|
var id_text := raw.trim_prefix(prefix).trim_suffix(suffix)
|
||||||
|
return id_text.to_int()
|
||||||
|
|
||||||
|
|
||||||
func _execute_pantry_interaction(context: PlayerInteractionResult) -> void:
|
func _execute_pantry_interaction(context: PlayerInteractionResult) -> void:
|
||||||
if not context.is_available():
|
if not context.is_available():
|
||||||
interaction_feedback.emit("The pantry is empty", context.blocked_reason, false)
|
interaction_feedback.emit("The pantry is empty", context.blocked_reason, false)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ var event_knowledge_system := EventKnowledgeSystemScript.new()
|
|||||||
var opportunity_system := VillageOpportunitySystem.new()
|
var opportunity_system := VillageOpportunitySystem.new()
|
||||||
var player_quest_system := PlayerQuestSystem.new()
|
var player_quest_system := PlayerQuestSystem.new()
|
||||||
var player_needs := PlayerNeedsSystem.new()
|
var player_needs := PlayerNeedsSystem.new()
|
||||||
|
var player_negotiation := PlayerNegotiationSystem.new()
|
||||||
var _last_player_tier := PlayerStandingRecord.TIER_STRANGER
|
var _last_player_tier := PlayerStandingRecord.TIER_STRANGER
|
||||||
var storage_states: Dictionary:
|
var storage_states: Dictionary:
|
||||||
get:
|
get:
|
||||||
@@ -88,6 +89,15 @@ func _ready() -> void:
|
|||||||
event_recorder.set_npcs(npcs)
|
event_recorder.set_npcs(npcs)
|
||||||
player_needs.configure(economy, event_recorder)
|
player_needs.configure(economy, event_recorder)
|
||||||
player_needs.set_resource_states(resource_states)
|
player_needs.set_resource_states(resource_states)
|
||||||
|
player_negotiation.configure(
|
||||||
|
player_quest_system,
|
||||||
|
Callable(self, "_find_npc_by_id"),
|
||||||
|
Callable(self, "get_active_opportunity"),
|
||||||
|
Callable(self, "get_active_opportunity_player_response"),
|
||||||
|
player_quest_opened,
|
||||||
|
player_quest_expired,
|
||||||
|
player_standing_changed
|
||||||
|
)
|
||||||
economy.inventory_changed.connect(_on_economy_inventory_changed)
|
economy.inventory_changed.connect(_on_economy_inventory_changed)
|
||||||
economy.economic_event_requested.connect(event_recorder.record_economic)
|
economy.economic_event_requested.connect(event_recorder.record_economic)
|
||||||
economy.narrative_event_requested.connect(event_recorder.record_narrative)
|
economy.narrative_event_requested.connect(event_recorder.record_narrative)
|
||||||
@@ -181,7 +191,10 @@ func simulate_tick() -> void:
|
|||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("--- Tick ", tick_count, " ---")
|
print("--- Tick ", tick_count, " ---")
|
||||||
animal_care.advance(tick_count)
|
animal_care.advance(tick_count)
|
||||||
_consider_animal_care_quests()
|
for quest in player_quest_system.consider_animal_care_quests(
|
||||||
|
animal_care, npcs, get_pantry(), tick_count
|
||||||
|
):
|
||||||
|
player_quest_opened.emit(quest)
|
||||||
_advance_player_needs()
|
_advance_player_needs()
|
||||||
var village_was_changed := false
|
var village_was_changed := false
|
||||||
_population_view.rebuild(npcs)
|
_population_view.rebuild(npcs)
|
||||||
@@ -594,15 +607,15 @@ func _finish_quest_resolution(resolution: Dictionary) -> void:
|
|||||||
if not resolution.has("quest"):
|
if not resolution.has("quest"):
|
||||||
return
|
return
|
||||||
var quest: PlayerQuestRecord = resolution["quest"]
|
var quest: PlayerQuestRecord = resolution["quest"]
|
||||||
if not resolution["completed"]:
|
if resolution["completed"]:
|
||||||
player_quest_expired.emit(quest)
|
player_quest_completed.emit(quest)
|
||||||
|
var standing := player_quest_system.standing
|
||||||
|
player_standing_changed.emit(standing)
|
||||||
|
if standing.get_tier() > _last_player_tier:
|
||||||
|
_last_player_tier = standing.get_tier()
|
||||||
|
player_tier_advanced.emit(standing.get_tier(), standing.get_tier_name())
|
||||||
return
|
return
|
||||||
player_quest_completed.emit(quest)
|
player_quest_expired.emit(quest)
|
||||||
var standing := player_quest_system.standing
|
|
||||||
player_standing_changed.emit(standing)
|
|
||||||
if standing.get_tier() > _last_player_tier:
|
|
||||||
_last_player_tier = standing.get_tier()
|
|
||||||
player_tier_advanced.emit(standing.get_tier(), standing.get_tier_name())
|
|
||||||
|
|
||||||
|
|
||||||
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool:
|
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool:
|
||||||
@@ -764,10 +777,8 @@ func get_player_state() -> PlayerStateRecord:
|
|||||||
|
|
||||||
|
|
||||||
func player_eat(amount: float) -> float:
|
func player_eat(amount: float) -> float:
|
||||||
var eaten := player_needs.eat(amount)
|
player_needs_changed.emit(player_needs.state)
|
||||||
if eaten > 0.0:
|
return player_needs.eat(amount)
|
||||||
player_needs_changed.emit(player_needs.state)
|
|
||||||
return eaten
|
|
||||||
|
|
||||||
|
|
||||||
func player_gather(node: ResourceNode) -> float:
|
func player_gather(node: ResourceNode) -> float:
|
||||||
@@ -785,33 +796,22 @@ func player_deposit(resource_id: StringName) -> float:
|
|||||||
return deposited
|
return deposited
|
||||||
|
|
||||||
|
|
||||||
func _consider_animal_care_quests() -> void:
|
|
||||||
var pantry: StorageStateRecord = get_pantry()
|
|
||||||
for animal_state in animal_care.get_all_states():
|
|
||||||
var quest := player_quest_system.consider_animal_care(
|
|
||||||
animal_state, _nearest_npc_id(animal_state.get_position()), tick_count, pantry
|
|
||||||
)
|
|
||||||
if quest != null:
|
|
||||||
player_quest_opened.emit(quest)
|
|
||||||
|
|
||||||
|
|
||||||
func _nearest_npc_id(from_position: Vector3) -> int:
|
|
||||||
var nearest_id := -1
|
|
||||||
var nearest_distance := INF
|
|
||||||
for npc in npcs:
|
|
||||||
if npc.is_dead:
|
|
||||||
continue
|
|
||||||
var distance := npc.position.distance_squared_to(from_position)
|
|
||||||
if distance < nearest_distance:
|
|
||||||
nearest_distance = distance
|
|
||||||
nearest_id = npc.id
|
|
||||||
return nearest_id
|
|
||||||
|
|
||||||
|
|
||||||
func get_player_quests() -> Array[PlayerQuestRecord]:
|
func get_player_quests() -> Array[PlayerQuestRecord]:
|
||||||
return player_quest_system.get_all_sorted()
|
return player_quest_system.get_all_sorted()
|
||||||
|
|
||||||
|
|
||||||
|
func get_villager_talk(npc_id: int) -> PlayerTalkResult:
|
||||||
|
return player_negotiation.get_talk(npc_id)
|
||||||
|
|
||||||
|
|
||||||
|
func accept_villager_request(npc_id: int) -> PlayerQuestRecord:
|
||||||
|
return player_negotiation.accept_request(npc_id, tick_count)
|
||||||
|
|
||||||
|
|
||||||
|
func decline_villager_request(npc_id: int) -> PlayerQuestRecord:
|
||||||
|
return player_negotiation.decline_request(npc_id, tick_count)
|
||||||
|
|
||||||
|
|
||||||
func get_latest_player_quest_for_requester(requester_npc_id: int) -> PlayerQuestRecord:
|
func get_latest_player_quest_for_requester(requester_npc_id: int) -> PlayerQuestRecord:
|
||||||
return player_quest_system.get_latest_for_requester(requester_npc_id)
|
return player_quest_system.get_latest_for_requester(requester_npc_id)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
class_name PlayerNegotiationSystem
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var quest_system: PlayerQuestSystem
|
||||||
|
var npc_lookup: Callable
|
||||||
|
var opportunity_provider: Callable
|
||||||
|
var player_response_provider: Callable
|
||||||
|
var quest_opened_signal: Signal
|
||||||
|
var quest_expired_signal: Signal
|
||||||
|
var standing_changed_signal: Signal
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
quests: PlayerQuestSystem,
|
||||||
|
lookup: Callable,
|
||||||
|
opportunity_source: Callable,
|
||||||
|
response_source: Callable,
|
||||||
|
opened: Signal,
|
||||||
|
expired: Signal,
|
||||||
|
standing: Signal
|
||||||
|
) -> void:
|
||||||
|
quest_system = quests
|
||||||
|
npc_lookup = lookup
|
||||||
|
opportunity_provider = opportunity_source
|
||||||
|
player_response_provider = response_source
|
||||||
|
quest_opened_signal = opened
|
||||||
|
quest_expired_signal = expired
|
||||||
|
standing_changed_signal = standing
|
||||||
|
|
||||||
|
|
||||||
|
func get_talk(npc_id: int) -> PlayerTalkResult:
|
||||||
|
var npc := npc_lookup.call(npc_id) as SimNPC
|
||||||
|
if npc == null or npc.is_dead:
|
||||||
|
return null
|
||||||
|
return quest_system.get_talk_for_requester(npc.id, npc.npc_name, opportunity_provider.call())
|
||||||
|
|
||||||
|
|
||||||
|
func accept_request(npc_id: int, current_tick: int) -> PlayerQuestRecord:
|
||||||
|
var npc := npc_lookup.call(npc_id) as SimNPC
|
||||||
|
if npc == null:
|
||||||
|
return null
|
||||||
|
var opportunity: OpportunityStateRecord = opportunity_provider.call()
|
||||||
|
if opportunity == null or opportunity.get_interested_npc_id() != npc.id:
|
||||||
|
return null
|
||||||
|
var result: Dictionary = quest_system.accept_opportunity_quest(
|
||||||
|
opportunity, player_response_provider.call(), current_tick
|
||||||
|
)
|
||||||
|
var quest := result.get("quest") as PlayerQuestRecord
|
||||||
|
if quest != null and result.get("created", false) and not quest_opened_signal.is_null():
|
||||||
|
quest_opened_signal.emit(quest)
|
||||||
|
return quest
|
||||||
|
|
||||||
|
|
||||||
|
func decline_request(npc_id: int, current_tick: int) -> PlayerQuestRecord:
|
||||||
|
var npc := npc_lookup.call(npc_id) as SimNPC
|
||||||
|
if npc == null:
|
||||||
|
return null
|
||||||
|
var opportunity: OpportunityStateRecord = opportunity_provider.call()
|
||||||
|
if opportunity == null or opportunity.get_interested_npc_id() != npc.id:
|
||||||
|
return null
|
||||||
|
var quest := quest_system.decline_opportunity_quest(opportunity, current_tick)
|
||||||
|
if quest != null:
|
||||||
|
if not quest_expired_signal.is_null():
|
||||||
|
quest_expired_signal.emit(quest)
|
||||||
|
if not standing_changed_signal.is_null():
|
||||||
|
standing_changed_signal.emit(quest_system.standing)
|
||||||
|
return quest
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://7oayv5ga327o
|
||||||
@@ -135,6 +135,58 @@ func on_opportunity_resolved(
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func get_talk_for_requester(
|
||||||
|
requester_id: int, requester_name: String, opportunity: OpportunityStateRecord
|
||||||
|
) -> PlayerTalkResult:
|
||||||
|
var has_need := opportunity != null and opportunity.is_open()
|
||||||
|
var need_text := ""
|
||||||
|
if has_need:
|
||||||
|
need_text = "I'm worried about the %s." % String(opportunity.get_resource_id()).capitalize()
|
||||||
|
var quest := get_latest_for_requester(requester_id)
|
||||||
|
var quest_available := quest != null and quest.is_open()
|
||||||
|
var reward := quest.get_standing_reward() if quest_available else 0.0
|
||||||
|
var can_accept := has_need and (quest_available or can_request_personally(opportunity))
|
||||||
|
return PlayerTalkResult.new(
|
||||||
|
requester_id,
|
||||||
|
requester_name,
|
||||||
|
"%s greets you." % requester_name,
|
||||||
|
has_need,
|
||||||
|
need_text,
|
||||||
|
quest_available,
|
||||||
|
reward,
|
||||||
|
can_accept,
|
||||||
|
has_need
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func accept_opportunity_quest(
|
||||||
|
opportunity: OpportunityStateRecord,
|
||||||
|
player_response: OpportunityPlayerResponseResult,
|
||||||
|
current_tick: int
|
||||||
|
) -> Dictionary:
|
||||||
|
var existing := _find_open_quest_for_opportunity(opportunity.get_opportunity_id())
|
||||||
|
if existing != null:
|
||||||
|
return {"quest": existing, "created": false}
|
||||||
|
var created := consider_opportunity_opened(
|
||||||
|
opportunity, player_response, current_tick, can_request_personally(opportunity)
|
||||||
|
)
|
||||||
|
return {"quest": created, "created": created != null}
|
||||||
|
|
||||||
|
|
||||||
|
func decline_opportunity_quest(
|
||||||
|
opportunity: OpportunityStateRecord, current_tick: int
|
||||||
|
) -> PlayerQuestRecord:
|
||||||
|
var quest := _find_open_quest_for_opportunity(opportunity.get_opportunity_id())
|
||||||
|
if quest == null:
|
||||||
|
return null
|
||||||
|
if not quest.expire(current_tick):
|
||||||
|
return null
|
||||||
|
var requester_id := quest.get_requester_npc_id()
|
||||||
|
var gratitude := standing.get_gratitude(requester_id)
|
||||||
|
standing.data["gratitude"][requester_id] = maxf(gratitude - 0.1, 0.0)
|
||||||
|
return quest
|
||||||
|
|
||||||
|
|
||||||
func on_opportunity_invalidated(
|
func on_opportunity_invalidated(
|
||||||
opportunity: OpportunityStateRecord, current_tick: int
|
opportunity: OpportunityStateRecord, current_tick: int
|
||||||
) -> PlayerQuestRecord:
|
) -> PlayerQuestRecord:
|
||||||
@@ -146,6 +198,34 @@ func on_opportunity_invalidated(
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func consider_animal_care_quests(
|
||||||
|
animal_care: AnimalCareSystem,
|
||||||
|
npcs: Array[SimNPC],
|
||||||
|
pantry: StorageStateRecord,
|
||||||
|
current_tick: int
|
||||||
|
) -> Array[PlayerQuestRecord]:
|
||||||
|
var opened: Array[PlayerQuestRecord] = []
|
||||||
|
for animal_state in animal_care.get_all_states():
|
||||||
|
var requester := _nearest_npc_id(animal_state.get_position(), npcs)
|
||||||
|
var quest := consider_animal_care(animal_state, requester, current_tick, pantry)
|
||||||
|
if quest != null:
|
||||||
|
opened.append(quest)
|
||||||
|
return opened
|
||||||
|
|
||||||
|
|
||||||
|
static func _nearest_npc_id(from_position: Vector3, npcs: Array[SimNPC]) -> int:
|
||||||
|
var nearest_id := -1
|
||||||
|
var nearest_distance := INF
|
||||||
|
for npc in npcs:
|
||||||
|
if npc.is_dead:
|
||||||
|
continue
|
||||||
|
var distance := npc.position.distance_squared_to(from_position)
|
||||||
|
if distance < nearest_distance:
|
||||||
|
nearest_distance = distance
|
||||||
|
nearest_id = npc.id
|
||||||
|
return nearest_id
|
||||||
|
|
||||||
|
|
||||||
func get_active_quest() -> PlayerQuestRecord:
|
func get_active_quest() -> PlayerQuestRecord:
|
||||||
for quest in quests:
|
for quest in quests:
|
||||||
if quest.is_open():
|
if quest.is_open():
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var opened_quest_ids: Array[int] = []
|
||||||
|
var expired_quest_ids: Array[int] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var manager := _create_manager()
|
||||||
|
manager.player_quest_opened.connect(_on_quest_opened)
|
||||||
|
manager.player_quest_expired.connect(_on_quest_expired)
|
||||||
|
var berry := _register_resource(manager, &"talk_berry_bush")
|
||||||
|
var actor: SimNPC = manager.npcs[0]
|
||||||
|
var interested: SimNPC = manager.npcs[2]
|
||||||
|
_set_pantry_amount(manager, 1.0)
|
||||||
|
for npc in manager.npcs:
|
||||||
|
npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0)
|
||||||
|
actor.position = Vector3.ZERO
|
||||||
|
interested.position = Vector3(4.0, 0.0, 0.0)
|
||||||
|
actor.hunger = 60.0
|
||||||
|
interested.hunger = 90.0
|
||||||
|
manager.economy.withdraw_to_inventory(actor, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
|
var opportunity: OpportunityStateRecord = manager.get_active_opportunity()
|
||||||
|
_check(
|
||||||
|
opportunity != null and opportunity.get_interested_npc_id() == interested.id,
|
||||||
|
"The talk proof needs an open need owned by the inspected villager",
|
||||||
|
)
|
||||||
|
if opportunity == null:
|
||||||
|
berry.free()
|
||||||
|
manager.free()
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
|
||||||
|
var talk: PlayerTalkResult = manager.get_villager_talk(interested.id)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
talk != null
|
||||||
|
and talk.npc_id == interested.id
|
||||||
|
and talk.npc_name == interested.npc_name
|
||||||
|
and talk.has_need
|
||||||
|
and talk.can_accept
|
||||||
|
and interested.npc_name in talk.greeting
|
||||||
|
),
|
||||||
|
"Talking should greet the villager and surface their real need",
|
||||||
|
)
|
||||||
|
|
||||||
|
var checksum_before: String = manager.get_state_checksum()
|
||||||
|
var accepted: PlayerQuestRecord = manager.accept_villager_request(interested.id)
|
||||||
|
print(
|
||||||
|
" debug accepted=",
|
||||||
|
accepted,
|
||||||
|
" opened=",
|
||||||
|
opened_quest_ids,
|
||||||
|
" active=",
|
||||||
|
manager.get_active_player_quest(),
|
||||||
|
" checksum_same=",
|
||||||
|
manager.get_state_checksum() == checksum_before,
|
||||||
|
" resolved=",
|
||||||
|
manager.get_player_standing().get_resolved_needs(),
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
accepted != null
|
||||||
|
and accepted.is_open()
|
||||||
|
and accepted.get_requester_npc_id() == interested.id
|
||||||
|
and opened_quest_ids == [accepted.get_quest_id()]
|
||||||
|
and manager.get_state_checksum() == checksum_before
|
||||||
|
and manager.get_player_standing().get_resolved_needs() == 0
|
||||||
|
),
|
||||||
|
"Accepting a request should surface the same open quest without granting standing yet",
|
||||||
|
)
|
||||||
|
var accepted_again: PlayerQuestRecord = manager.accept_villager_request(interested.id)
|
||||||
|
_check(
|
||||||
|
accepted_again != null and accepted_again.get_quest_id() == accepted.get_quest_id(),
|
||||||
|
"Re-accepting should not duplicate the open quest",
|
||||||
|
)
|
||||||
|
|
||||||
|
var carry_state: PlayerStateRecord = manager.get_player_state()
|
||||||
|
carry_state.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
|
var deposited: float = manager.player_deposit(SimulationIds.RESOURCE_FOOD)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
is_equal_approx(deposited, 1.0)
|
||||||
|
and manager.get_active_player_quest() == null
|
||||||
|
and manager.get_player_standing().get_resolved_needs() == 1
|
||||||
|
),
|
||||||
|
"Completing the accepted request through a real deposit should resolve it",
|
||||||
|
)
|
||||||
|
|
||||||
|
var declined := _create_manager(904)
|
||||||
|
declined.player_quest_opened.connect(_on_quest_opened)
|
||||||
|
declined.player_quest_expired.connect(_on_quest_expired)
|
||||||
|
var berry2 := _register_resource(declined, &"talk_berry_bush_2")
|
||||||
|
var actor2: SimNPC = declined.npcs[0]
|
||||||
|
var interested2: SimNPC = declined.npcs[2]
|
||||||
|
_set_pantry_amount(declined, 1.0)
|
||||||
|
for npc in declined.npcs:
|
||||||
|
npc.position = Vector3(40.0 + npc.id * 10.0, 0.0, 0.0)
|
||||||
|
actor2.position = Vector3.ZERO
|
||||||
|
interested2.position = Vector3(4.0, 0.0, 0.0)
|
||||||
|
actor2.hunger = 60.0
|
||||||
|
interested2.hunger = 90.0
|
||||||
|
declined.economy.withdraw_to_inventory(actor2, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
|
var declined_quest: PlayerQuestRecord = declined.get_active_player_quest()
|
||||||
|
_check(declined_quest != null, "The decline branch needs an open quest")
|
||||||
|
if declined_quest == null:
|
||||||
|
berry.free()
|
||||||
|
berry2.free()
|
||||||
|
manager.free()
|
||||||
|
declined.free()
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
var gratitude_before: float = declined.get_player_standing().get_gratitude(interested2.id)
|
||||||
|
var refused: PlayerQuestRecord = declined.decline_villager_request(interested2.id)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
refused != null
|
||||||
|
and refused.get_quest_id() == declined_quest.get_quest_id()
|
||||||
|
and expired_quest_ids.has(declined_quest.get_quest_id())
|
||||||
|
and declined.get_active_player_quest() == null
|
||||||
|
and declined.get_player_standing().get_gratitude(interested2.id) <= gratitude_before
|
||||||
|
),
|
||||||
|
"Declining a personal request should close the quest without granting gratitude",
|
||||||
|
)
|
||||||
|
|
||||||
|
berry.free()
|
||||||
|
berry2.free()
|
||||||
|
manager.free()
|
||||||
|
declined.free()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _register_resource(manager: Node, node_id: StringName) -> ResourceNode:
|
||||||
|
var node := ResourceNode.new()
|
||||||
|
node.name = "TalkResource"
|
||||||
|
node.node_id = node_id
|
||||||
|
node.action_id = SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
node.resource_id = SimulationIds.RESOURCE_FOOD
|
||||||
|
node.initial_amount = 2.0
|
||||||
|
node.yield_per_action = 2.0
|
||||||
|
node.debug_label_enabled = false
|
||||||
|
var interaction_point := Marker3D.new()
|
||||||
|
interaction_point.name = "InteractionPoint"
|
||||||
|
node.add_child(interaction_point)
|
||||||
|
root.add_child(node)
|
||||||
|
_check(
|
||||||
|
manager.register_resource_node(node),
|
||||||
|
"The talk proof should bind a real finite food ResourceNode"
|
||||||
|
)
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
func _create_manager(seed_value: int = 901) -> Node:
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.simulation_seed = seed_value
|
||||||
|
manager.debug_logs = false
|
||||||
|
var home_positions: Array[Vector3] = [
|
||||||
|
Vector3(0.0, 0.0, 0.0),
|
||||||
|
Vector3(2.0, 0.0, 0.0),
|
||||||
|
Vector3(10.0, 0.0, 0.0),
|
||||||
|
Vector3(12.0, 0.0, 0.0),
|
||||||
|
Vector3(30.0, 0.0, 30.0),
|
||||||
|
Vector3(40.0, 0.0, 40.0),
|
||||||
|
]
|
||||||
|
manager.home_positions = home_positions
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
func _set_pantry_amount(manager: Node, amount: float) -> void:
|
||||||
|
var pantry: StorageStateRecord = manager.get_pantry()
|
||||||
|
pantry.withdraw(SimulationIds.RESOURCE_FOOD, pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
||||||
|
pantry.deposit(SimulationIds.RESOURCE_FOOD, amount)
|
||||||
|
manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_quest_opened(quest: PlayerQuestRecord) -> void:
|
||||||
|
opened_quest_ids.append(quest.get_quest_id())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_quest_expired(quest: PlayerQuestRecord) -> void:
|
||||||
|
expired_quest_ids.append(quest.get_quest_id())
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Player talk and personal negotiation passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cdh2d6y6jvajo
|
||||||
Reference in New Issue
Block a user