feat: let the player talk to villagers and accept or decline personal requests

This commit is contained in:
2026-08-09 00:10:36 +02:00
parent 5e6c963d23
commit 0936ae6450
10 changed files with 520 additions and 36 deletions
+36 -36
View File
@@ -56,6 +56,7 @@ var event_knowledge_system := EventKnowledgeSystemScript.new()
var opportunity_system := VillageOpportunitySystem.new()
var player_quest_system := PlayerQuestSystem.new()
var player_needs := PlayerNeedsSystem.new()
var player_negotiation := PlayerNegotiationSystem.new()
var _last_player_tier := PlayerStandingRecord.TIER_STRANGER
var storage_states: Dictionary:
get:
@@ -88,6 +89,15 @@ func _ready() -> void:
event_recorder.set_npcs(npcs)
player_needs.configure(economy, event_recorder)
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.economic_event_requested.connect(event_recorder.record_economic)
economy.narrative_event_requested.connect(event_recorder.record_narrative)
@@ -181,7 +191,10 @@ func simulate_tick() -> void:
if debug_logs:
print("--- Tick ", 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()
var village_was_changed := false
_population_view.rebuild(npcs)
@@ -594,15 +607,15 @@ func _finish_quest_resolution(resolution: Dictionary) -> void:
if not resolution.has("quest"):
return
var quest: PlayerQuestRecord = resolution["quest"]
if not resolution["completed"]:
player_quest_expired.emit(quest)
if resolution["completed"]:
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
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())
player_quest_expired.emit(quest)
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:
var eaten := player_needs.eat(amount)
if eaten > 0.0:
player_needs_changed.emit(player_needs.state)
return eaten
player_needs_changed.emit(player_needs.state)
return player_needs.eat(amount)
func player_gather(node: ResourceNode) -> float:
@@ -785,33 +796,22 @@ func player_deposit(resource_id: StringName) -> float:
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]:
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:
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
+80
View File
@@ -135,6 +135,58 @@ func on_opportunity_resolved(
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(
opportunity: OpportunityStateRecord, current_tick: int
) -> PlayerQuestRecord:
@@ -146,6 +198,34 @@ func on_opportunity_invalidated(
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:
for quest in quests:
if quest.is_open():