284 lines
8.5 KiB
GDScript
284 lines
8.5 KiB
GDScript
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()
|