feat: let trusted villagers ask the player directly as standing grows

This commit is contained in:
2026-08-08 20:35:07 +02:00
parent 6cdeea6e46
commit 3b6e35ba6d
5 changed files with 122 additions and 24 deletions
+27 -17
View File
@@ -28,6 +28,7 @@ signal player_quest_opened(quest: PlayerQuestRecord)
signal player_quest_completed(quest: PlayerQuestRecord)
signal player_quest_expired(quest: PlayerQuestRecord)
signal player_standing_changed(standing: PlayerStandingRecord)
signal player_tier_advanced(tier: int, tier_name: String)
var village := SimVillage.new()
@@ -51,6 +52,7 @@ var relationship_system := RelationshipSystemScript.new()
var event_knowledge_system := EventKnowledgeSystemScript.new()
var opportunity_system := VillageOpportunitySystem.new()
var player_quest_system := PlayerQuestSystem.new()
var _last_player_tier := PlayerStandingRecord.TIER_STRANGER
var storage_states: Dictionary:
get:
return economy.storage_states
@@ -578,19 +580,30 @@ func _on_economic_event_recorded(event: EconomicEventRecord) -> void:
func _resolve_animal_care_quest(event: EconomicEventRecord) -> void:
var animal_id := StringName(event.data["destination_id"])
var animal_state := animal_care.get_state(animal_id)
var animal_state := animal_care.get_state(StringName(event.data["destination_id"]))
var resolution := player_quest_system.on_animal_fed(animal_state, event, tick_count)
_finish_quest_resolution(resolution)
func _finish_quest_resolution(resolution: Dictionary) -> void:
if not resolution.has("quest"):
return
var quest: PlayerQuestRecord = resolution["quest"]
if resolution["completed"]:
player_quest_completed.emit(quest)
player_standing_changed.emit(player_quest_system.standing)
_emit_standing_change_and_tier()
else:
player_quest_expired.emit(quest)
func _emit_standing_change_and_tier() -> void:
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:
var speaker := _find_npc_by_id(speaker_id)
var listener := _find_npc_by_id(listener_id)
@@ -674,20 +687,18 @@ func _update_opportunity_from_event(event: EconomicEventRecord) -> void:
if changed.get_status() == OpportunityStateRecord.STATUS_OPEN:
opportunity_opened.emit(changed)
var quest := player_quest_system.consider_opportunity_opened(
changed, get_active_opportunity_player_response(), tick_count
changed,
get_active_opportunity_player_response(),
tick_count,
player_quest_system.can_request_personally(changed)
)
if quest != null:
player_quest_opened.emit(quest)
return
opportunity_resolved.emit(changed, event)
var resolution := player_quest_system.on_opportunity_resolved(changed, event, tick_count)
if resolution.has("quest"):
var quest: PlayerQuestRecord = resolution["quest"]
if resolution["completed"]:
player_quest_completed.emit(quest)
player_standing_changed.emit(player_quest_system.standing)
else:
player_quest_expired.emit(quest)
_finish_quest_resolution(
player_quest_system.on_opportunity_resolved(changed, event, tick_count)
)
_maintain_event_knowledge(false)
@@ -780,22 +791,20 @@ func get_active_player_quest() -> PlayerQuestRecord:
func _consider_animal_care_quests() -> void:
var pantry: StorageStateRecord = get_pantry()
for animal_state in animal_care.get_all_states():
var requester := _find_animal_requester(animal_state)
var quest := player_quest_system.consider_animal_care(
animal_state, requester, tick_count, pantry
animal_state, _nearest_npc_id(animal_state.get_position()), tick_count, pantry
)
if quest != null:
player_quest_opened.emit(quest)
func _find_animal_requester(animal_state: AnimalStateRecord) -> int:
var animal_position := animal_state.get_position()
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(animal_position)
var distance := npc.position.distance_squared_to(from_position)
if distance < nearest_distance:
nearest_distance = distance
nearest_id = npc.id
@@ -1172,6 +1181,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
player_quest_system.restore(
record.player_quests, int(record.simulation.get("next_quest_id", 0)), record.player_standing
)
_last_player_tier = player_quest_system.standing.get_tier()
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
wander_random_sources.clear()
var wander_streams: Array = record.simulation["wander_random_streams"]
+17 -7
View File
@@ -4,6 +4,8 @@ extends RefCounted
const PANTRY_STANDING_REWARD := 8.0
const WOOD_STANDING_REWARD := 8.0
const ANIMAL_CARE_STANDING_REWARD := 6.0
const PERSONAL_TRUST_THRESHOLD := 0.5
const MIN_PERSONAL_REQUEST_TIER := PlayerStandingRecord.TIER_KNOWN_HAND
var quests: Array[PlayerQuestRecord] = []
var next_quest_id := 0
@@ -77,14 +79,12 @@ func on_animal_fed(
func consider_opportunity_opened(
opportunity: OpportunityStateRecord,
player_response: OpportunityPlayerResponseResult,
current_tick: int
current_tick: int,
personal_request_eligible := false
) -> PlayerQuestRecord:
if (
opportunity == null
or not opportunity.is_open()
or player_response == null
or current_tick < 0
):
if opportunity == null or not opportunity.is_open() or current_tick < 0:
return null
if player_response == null and not personal_request_eligible:
return null
if _find_open_quest_for_opportunity(opportunity.get_opportunity_id()) != null:
return null
@@ -107,6 +107,16 @@ func consider_opportunity_opened(
return quest
func can_request_personally(opportunity: OpportunityStateRecord) -> bool:
if (
opportunity == null
or not opportunity.is_open()
or standing.get_tier() < MIN_PERSONAL_REQUEST_TIER
):
return false
return standing.get_gratitude(opportunity.get_interested_npc_id()) >= PERSONAL_TRUST_THRESHOLD
func on_opportunity_resolved(
opportunity: OpportunityStateRecord, resolution_event: EconomicEventRecord, current_tick: int
) -> Dictionary:
+70
View File
@@ -0,0 +1,70 @@
extends GutTest
const PlayerQuestSystemScript := preload("res://simulation/quests/PlayerQuestSystem.gd")
func test_personal_request_needs_standing_tier_and_gratitude() -> void:
var system := PlayerQuestSystemScript.new()
var opportunity := _open_opportunity()
var pantry := _pantry()
assert_false(system.can_request_personally(opportunity))
for _grant in 6:
system.standing.grant_standing(3.0, opportunity.get_interested_npc_id())
assert_true(
system.can_request_personally(opportunity),
"Known Hand standing plus personal gratitude should make the NPC ask you directly"
)
var quest := system.consider_opportunity_opened(opportunity, null, 10, true)
assert_not_null(quest)
assert_eq(quest.get_requester_npc_id(), opportunity.get_interested_npc_id())
func test_no_personal_request_below_trust_threshold() -> void:
var system := PlayerQuestSystemScript.new()
var opportunity := _open_opportunity()
system.standing.grant_standing(16.0, opportunity.get_interested_npc_id())
system.standing.data["gratitude"][opportunity.get_interested_npc_id()] = 0.4
assert_false(system.can_request_personally(opportunity))
func test_standing_tier_progression() -> void:
var standing := PlayerStandingRecord.create()
assert_eq(standing.get_tier(), PlayerStandingRecord.TIER_STRANGER)
standing.grant_standing(14.0, 3)
assert_eq(standing.get_tier(), PlayerStandingRecord.TIER_STRANGER)
standing.grant_standing(2.0, 3)
assert_eq(standing.get_tier(), PlayerStandingRecord.TIER_KNOWN_HAND)
assert_eq(standing.get_tier_name(), "Known Hand")
standing.grant_standing(20.0, 3)
assert_eq(standing.get_tier(), PlayerStandingRecord.TIER_TRUSTED)
func test_standing_serialization_round_trip() -> void:
var standing := PlayerStandingRecord.create()
standing.grant_standing(35.0, 3)
var restored := PlayerStandingRecord.from_dictionary(standing.to_dictionary())
assert_not_null(restored)
assert_eq(restored.get_standing(), 35.0)
assert_eq(restored.get_tier(), PlayerStandingRecord.TIER_TRUSTED)
assert_eq(restored.get_gratitude(3), 0.1)
func _open_opportunity() -> OpportunityStateRecord:
return OpportunityStateRecord.create(
0,
10,
5,
3,
1.0,
SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY,
SimulationIds.STORAGE_VILLAGE_PANTRY,
SimulationIds.RESOURCE_FOOD
)
func _pantry() -> StorageStateRecord:
return StorageStateRecord.create(
SimulationIds.STORAGE_VILLAGE_PANTRY, {SimulationIds.RESOURCE_FOOD: 0.0}, 100.0
)
@@ -0,0 +1 @@
uid://b2jmmtiyvaqll
+7
View File
@@ -34,6 +34,8 @@ func _ready() -> void:
simulation_manager.player_quest_expired.connect(_on_quest_expired)
if simulation_manager.has_signal("player_standing_changed"):
simulation_manager.player_standing_changed.connect(_on_standing_changed)
if simulation_manager.has_signal("player_tier_advanced"):
simulation_manager.player_tier_advanced.connect(_on_tier_advanced)
if simulation_manager.has_signal("opportunity_resolved"):
simulation_manager.opportunity_resolved.connect(_on_opportunity_resolved)
if simulation_manager.has_signal("state_restored"):
@@ -53,6 +55,11 @@ func _on_standing_changed(_standing: PlayerStandingRecord) -> void:
_refresh()
func _on_tier_advanced(_tier: int, tier_name: String) -> void:
_refresh()
_show_thanks("Standing grows", "The village now calls you a %s." % tier_name)
func _on_opportunity_resolved(
_opportunity: OpportunityStateRecord, _cause_event: EconomicEventRecord
) -> void: