class_name PlayerQuestSystem 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 var standing := PlayerStandingRecord.create() func consider_animal_care( animal_state: AnimalStateRecord, requester_npc_id: int, current_tick: int, pantry: StorageStateRecord ) -> PlayerQuestRecord: if ( animal_state == null or requester_npc_id < 0 or current_tick < 0 or pantry == null or not animal_state.needs_feed() or not animal_state.can_player_feed_animal() ): return null var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL) if ( definition == null or not definition.has_completion_cost() or ( pantry.get_amount(definition.completion_cost_resource_id) < definition.completion_cost_amount ) ): return null if _find_open_quest_for_animal(animal_state.get_animal_id()) != null: return null var quest := PlayerQuestRecord.create( next_quest_id, PlayerQuestRecord.NO_OPPORTUNITY, requester_npc_id, SimulationIds.OPPORTUNITY_FEED_HUNGRY_ANIMAL, definition.completion_cost_resource_id, animal_state.get_animal_id(), definition.completion_cost_amount, current_tick, ANIMAL_CARE_STANDING_REWARD, animal_state.get_animal_id() ) next_quest_id += 1 quests.append(quest) return quest func on_animal_fed( animal_state: AnimalStateRecord, resolution_event: EconomicEventRecord, current_tick: int ) -> Dictionary: if animal_state == null or resolution_event == null: return {} var quest := _find_open_quest_for_animal(animal_state.get_animal_id()) if quest == null: return {} var event_id := int(resolution_event.data["event_id"]) var actor_id := int(resolution_event.data["actor_id"]) if actor_id < 0: if not quest.complete(event_id, current_tick): return {} standing.grant_standing(quest.get_standing_reward(), quest.get_requester_npc_id()) return {"quest": quest, "completed": true} if quest.expire(current_tick): return {"quest": quest, "completed": false} return {} func consider_opportunity_opened( opportunity: OpportunityStateRecord, player_response: OpportunityPlayerResponseResult, current_tick: int, personal_request_eligible := false ) -> PlayerQuestRecord: 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 var reward := _standing_reward_for(opportunity) if reward <= 0.0: return null var quest := PlayerQuestRecord.create( next_quest_id, opportunity.get_opportunity_id(), opportunity.get_interested_npc_id(), opportunity.get_opportunity_type(), opportunity.get_resource_id(), opportunity.get_target_id(), opportunity.get_target_amount(), current_tick, reward ) next_quest_id += 1 quests.append(quest) 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: var quest := _find_open_quest_for_opportunity(opportunity.get_opportunity_id()) if quest == null or resolution_event == null: return {} var event_id := int(resolution_event.data["event_id"]) var actor_id := int(resolution_event.data["actor_id"]) if actor_id < 0: if not quest.complete(event_id, current_tick): return {} standing.grant_standing(quest.get_standing_reward(), quest.get_requester_npc_id()) return {"quest": quest, "completed": true} if quest.expire(current_tick): return {"quest": quest, "completed": false} 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: var quest := _find_open_quest_for_opportunity(opportunity.get_opportunity_id()) if quest == null: return null if quest.expire(current_tick): return quest 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(): return quest return null func get_all_sorted() -> Array[PlayerQuestRecord]: var sorted := quests.duplicate() sorted.sort_custom(_sort_by_id) return sorted func get_latest_for_requester(requester_npc_id: int) -> PlayerQuestRecord: for index in range(quests.size() - 1, -1, -1): var quest := quests[index] if quest.get_requester_npc_id() == requester_npc_id: return quest return null func restore( records: Array[PlayerQuestRecord], restored_next_id: int, standing_record: PlayerStandingRecord ) -> void: quests = records.duplicate() quests.sort_custom(_sort_by_id) next_quest_id = maxi(restored_next_id, 0) standing = standing_record if standing_record != null else PlayerStandingRecord.create() func _find_open_quest_for_opportunity(opportunity_id: int) -> PlayerQuestRecord: for quest in quests: if quest.is_open() and quest.get_opportunity_id() == opportunity_id: return quest return null func _find_open_quest_for_animal(animal_id: StringName) -> PlayerQuestRecord: for quest in quests: if quest.is_open() and quest.has_animal_target() and quest.get_animal_id() == animal_id: return quest return null func _standing_reward_for(opportunity: OpportunityStateRecord) -> float: match opportunity.get_opportunity_type(): SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY: return PANTRY_STANDING_REWARD SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD: return WOOD_STANDING_REWARD return 0.0 static func _sort_by_id(first: PlayerQuestRecord, second: PlayerQuestRecord) -> bool: return first.get_quest_id() < second.get_quest_id()