merge: integrate remote player quest slices

This commit is contained in:
Rijad Zuzo
2026-08-16 16:53:30 +02:00
55 changed files with 3640 additions and 54 deletions
+4
View File
@@ -7,6 +7,10 @@ const KIND_PANTRY := &"pantry"
const KIND_STORAGE_DEPOSIT := &"storage_deposit"
const KIND_ACTIVITY := &"activity"
const KIND_DIALOGUE := &"dialogue"
const KIND_DEPOSIT := &"deposit"
const KIND_TALK := &"talk"
const KIND_GUARD := &"guard"
const KIND_STUDY := &"study"
var kind: StringName
var action_id: StringName
+53
View File
@@ -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),
]
)
)
+1
View File
@@ -0,0 +1 @@
uid://i7pjb5605cgr
+4
View File
@@ -13,6 +13,7 @@ var target_name: String
var carried_amounts: Dictionary
var decision_reason: String
var _has_decision_reason: bool
var opportunity_note: VillagerOpportunityNote
var has_open_need := false
var need_type: StringName
@@ -46,6 +47,7 @@ func _init(
need_type = StringName(inspection_need.get("need_type", &""))
need_progress = String(inspection_need.get("progress", ""))
need_response = String(inspection_need.get("response", ""))
opportunity_note = inspection_need.get("opportunity_note") as VillagerOpportunityNote
func has_decision_reason() -> bool:
@@ -77,6 +79,7 @@ func cache_key() -> String:
var carried_parts: Array[String] = []
for item_id in carried_keys:
carried_parts.append("%s=%.3f" % [String(item_id), float(carried_amounts[item_id])])
var note_part := opportunity_note.cache_key() if opportunity_note != null else "-"
return (
"|"
. join(
@@ -94,6 +97,7 @@ func cache_key() -> String:
String(need_type),
need_progress,
need_response,
note_part,
]
)
)
+79
View File
@@ -0,0 +1,79 @@
class_name VillagerOpportunityNote
extends RefCounted
const HELP_PLAYER_ROUTE := &"player_route"
const HELP_HELPER := &"helper"
const HELP_UNAVAILABLE := &"unavailable"
var has_need := false
var need_text := ""
var help_kind: StringName = &""
var help_text := ""
func _init(
note_has_need := false,
note_need_text := "",
note_help_kind: StringName = &"",
note_help_text := ""
) -> void:
has_need = note_has_need
need_text = note_need_text
help_kind = note_help_kind
help_text = note_help_text
func cache_key() -> String:
return "%s|%s|%s|%s" % [has_need, need_text, String(help_kind), help_text]
static func derive(
opportunity: OpportunityStateRecord,
player_response: OpportunityPlayerResponseResult,
helper: OpportunityHelperResult,
helper_npc_name: String,
storage: StorageStateRecord,
target_display_name: String,
resource_display_name: String
) -> VillagerOpportunityNote:
if opportunity == null:
return VillagerOpportunityNote.new()
var need_text := "The %s needs %s" % [target_display_name, resource_display_name]
if player_response != null:
return (
VillagerOpportunityNote
. new(
true,
need_text,
HELP_PLAYER_ROUTE,
"You can help · forage %s to the %s" % [resource_display_name, target_display_name],
)
)
if helper != null:
return VillagerOpportunityNote.new(
true,
need_text,
HELP_HELPER,
"%s is bringing %s" % [helper_npc_name, resource_display_name]
)
if storage == null:
return VillagerOpportunityNote.new(
true, need_text, HELP_UNAVAILABLE, "No storage is within reach right now."
)
var remaining := maxf(
opportunity.get_target_amount() - storage.get_amount(opportunity.get_resource_id()), 0.0
)
if remaining <= 0.0:
return VillagerOpportunityNote.new(
true,
need_text,
HELP_UNAVAILABLE,
"The %s already has enough %s." % [target_display_name, resource_display_name]
)
if storage.get_available_capacity() < remaining:
return VillagerOpportunityNote.new(
true, need_text, HELP_UNAVAILABLE, "The %s has no room right now." % target_display_name
)
return VillagerOpportunityNote.new(
true, need_text, HELP_UNAVAILABLE, "No stocked source is in reach right now."
)
+1
View File
@@ -0,0 +1 @@
uid://ds7o7atkwyfon
+151 -6
View File
@@ -249,6 +249,7 @@ func get_nearby_villager_inspection() -> VillagerInspectionResult:
if decision != null and decision.action_id == npc.current_task:
reason = decision.reason
var need := _get_open_need_for_npc(npc)
need["opportunity_note"] = _build_opportunity_note(npc)
return VillagerInspectionResult.new(
npc.id,
npc.npc_name,
@@ -263,6 +264,57 @@ func get_nearby_villager_inspection() -> VillagerInspectionResult:
)
func _build_opportunity_note(npc: SimNPC) -> VillagerOpportunityNote:
if (
simulation_manager == null
or not simulation_manager.has_method("get_active_opportunity")
or not simulation_manager.has_method("get_active_opportunity_player_response")
or not simulation_manager.has_method("get_active_opportunity_helper")
):
return null
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
if (
opportunity == null
or opportunity.get_interested_npc_id() != npc.id
or (
opportunity.get_target_id()
not in [SimulationIds.STORAGE_VILLAGE_PANTRY, SimulationIds.STORAGE_VILLAGE_WOODPILE]
)
):
return null
var player_response: OpportunityPlayerResponseResult = (
simulation_manager.get_active_opportunity_player_response()
)
var helper: OpportunityHelperResult = simulation_manager.get_active_opportunity_helper()
var helper_name := ""
if helper != null:
var helper_npc := _find_npc(helper.helper_npc_id)
if helper_npc != null:
helper_name = helper_npc.npc_name
return VillagerOpportunityNote.derive(
opportunity,
player_response,
helper,
helper_name,
_get_opportunity_storage(opportunity),
_get_target_display_name(opportunity.get_target_id()),
_display_id(opportunity.get_resource_id()).to_lower()
)
func _get_opportunity_storage(opportunity: OpportunityStateRecord) -> StorageStateRecord:
if simulation_manager == null:
return null
if opportunity.get_target_id() == SimulationIds.STORAGE_VILLAGE_PANTRY:
return simulation_manager.get_pantry() as StorageStateRecord
if (
opportunity.get_target_id() == SimulationIds.STORAGE_VILLAGE_WOODPILE
and simulation_manager.has_method("get_woodpile")
):
return simulation_manager.get_woodpile() as StorageStateRecord
return null
func _get_open_need_for_npc(npc: SimNPC) -> Dictionary:
if not simulation_manager.has_method("get_latest_opportunity_for_npc"):
return {
@@ -439,14 +491,24 @@ func _build_resource_context(node: ResourceNode) -> PlayerInteractionResult:
definition.display_name if definition != null else String(node.action_id).capitalize()
)
var display_name := String(node.node_id).replace("_", " ").capitalize()
var carried := _get_player_carried(node.resource_id)
var detail := (
"%s · %.1f remaining · carrying %.1f" % [display_name, node.get_amount_remaining(), carried]
)
if _get_available_carry() <= 0.0:
detail = "Hands full — deposit before gathering"
return PlayerInteractionResult.new(
PlayerInteractionResult.KIND_RESOURCE,
node.action_id,
node.node_id,
display_name,
prompt,
"%s · %.1f remaining" % [display_name, node.get_amount_remaining()],
node
detail,
node,
{
"blocked_reason":
"" if _get_available_carry() > 0.0 else "You cannot carry more right now."
}
)
@@ -526,13 +588,19 @@ func _execute_animal_interaction(context: PlayerInteractionResult) -> void:
func _execute_resource_interaction(context: PlayerInteractionResult) -> void:
var node := context.target_node as ResourceNode
if node == null or not simulation_manager.has_method("harvest_resource_node"):
push_error("Player: SimulationManager cannot harvest the resolved ResourceNode")
if node == null or not simulation_manager.has_method("player_gather"):
push_error("Player: SimulationManager cannot gather the resolved ResourceNode")
return
var extracted: float = simulation_manager.harvest_resource_node(node)
var extracted: float = simulation_manager.player_gather(node)
if extracted <= 0.0:
interaction_feedback.emit(
"%s is unavailable" % context.display_name, "Nothing could be gathered.", false
"%s is unavailable" % context.display_name,
(
context.blocked_reason
if not context.blocked_reason.is_empty()
else "Nothing could be gathered."
),
false
)
return
interaction_feedback.emit(
@@ -582,6 +650,38 @@ func _execute_activity_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:
if not context.is_available():
interaction_feedback.emit("The pantry is empty", context.blocked_reason, false)
@@ -624,6 +724,37 @@ func _storage_name_for(resource_id: StringName) -> String:
return "village pantry" if resource_id == SimulationIds.RESOURCE_FOOD else "village woodpile"
func _get_player_hunger() -> float:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return 0.0
var state: PlayerStateRecord = simulation_manager.get_player_state()
return state.get_hunger() if state != null else 0.0
func is_starving() -> bool:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return false
var state: PlayerStateRecord = simulation_manager.get_player_state()
return state.is_starving() if state != null else false
func _needs_speed_factor() -> float:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return 1.0
var state: PlayerStateRecord = simulation_manager.get_player_state()
if state == null or state.is_dead():
return 0.0
var factor := 1.0
if state.is_starving():
factor *= 0.5
var energy := state.get_energy()
if energy < 20.0:
factor *= 0.6
elif energy < 45.0:
factor *= 0.8
return factor
func _get_animal_feed_cost() -> float:
var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
return definition.completion_cost_amount if definition != null else 1.0
@@ -648,6 +779,20 @@ func _get_carried_wood() -> float:
return simulation_manager.get_player_state().get_inventory_amount(SimulationIds.RESOURCE_WOOD)
func _get_player_carried(resource_id: StringName) -> float:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return 0.0
var state: PlayerStateRecord = simulation_manager.get_player_state()
return state.get_inventory_amount(resource_id) if state != null else 0.0
func _get_available_carry() -> float:
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
return 0.0
var state: PlayerStateRecord = simulation_manager.get_player_state()
return state.get_available_carry() if state != null else 0.0
func is_near_storage(storage_node: StorageNode) -> bool:
if storage_node == null:
return false