209 lines
7.7 KiB
GDScript
209 lines
7.7 KiB
GDScript
extends Resource
|
||
class_name CaptainProgress
|
||
|
||
const BALANCE := preload("res://scripts/captain/captain_balance.gd")
|
||
const SKILLS := preload("res://scripts/captain/captain_skills.gd")
|
||
const ACTIVE_SKILL_SLOTS := 5
|
||
const MAX_LUCK_CHANCE := 0.60
|
||
|
||
@export var level: int = 1
|
||
@export var experience: int = 0
|
||
@export var credits: int = 0
|
||
@export var parts: float = 0.0
|
||
@export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0, "sulfur": 0}
|
||
@export var expedition: Resource = preload("res://scripts/planets/planet_progress.gd").new()
|
||
@export var gear_items: Array[Dictionary] = []
|
||
@export var gear_slots: Dictionary = {}
|
||
@export var gear_next_uid := 1
|
||
@export var learning_points: int = 0
|
||
@export var skills: Dictionary = {}
|
||
@export var equipped_skills: Array[String] = ["", "", "", "", ""]
|
||
var skill_offers: Array[String] = []
|
||
var skill_cooldowns: Dictionary = {}
|
||
var skill_durations: Dictionary = {}
|
||
var _ammo_credit: Dictionary = {}
|
||
var _skill_rng := RandomNumberGenerator.new()
|
||
|
||
func _init() -> void:
|
||
_skill_rng.randomize()
|
||
preload("res://scripts/planets/captain_equipment.gd").initialize(self)
|
||
|
||
func get_xp_required() -> int:
|
||
return BALANCE.xp_required(level)
|
||
|
||
func get_skill_rank(skill: String) -> int:
|
||
return clampi(int(skills.get(skill, 0)), 0, SKILLS.MAX_RANK) if SKILLS.DATA.has(skill) else 0
|
||
|
||
func get_skill_bonus(skill: String) -> float:
|
||
return SKILLS.value(skill, get_skill_rank(skill))
|
||
|
||
func get_energy_bank_fraction() -> float:
|
||
return maxf(0.10, get_skill_bonus("electrician"))
|
||
|
||
func normalize_skill_ranks() -> void:
|
||
for skill in skills.keys():
|
||
if not SKILLS.DATA.has(skill):
|
||
skills.erase(skill)
|
||
else:
|
||
skills[skill] = get_skill_rank(skill)
|
||
normalize_equipped_skills()
|
||
|
||
func normalize_equipped_skills() -> void:
|
||
equipped_skills.resize(ACTIVE_SKILL_SLOTS)
|
||
var seen: Array[String] = []
|
||
for index in ACTIVE_SKILL_SLOTS:
|
||
var skill := equipped_skills[index]
|
||
if not SKILLS.DATA.has(skill) or not bool(SKILLS.DATA[skill]["active"]) or get_skill_rank(skill) <= 0 or skill in seen:
|
||
equipped_skills[index] = ""
|
||
else:
|
||
seen.append(skill)
|
||
|
||
func equip_skill(slot: int, skill: String) -> bool:
|
||
if slot < 0 or slot >= ACTIVE_SKILL_SLOTS:
|
||
return false
|
||
if not skill.is_empty() and (not SKILLS.DATA.has(skill) or not bool(SKILLS.DATA[skill]["active"]) or get_skill_rank(skill) <= 0):
|
||
return false
|
||
normalize_equipped_skills()
|
||
# Moving a skill between slots never resets its cooldown or active duration.
|
||
var previous_slot := equipped_skills.find(skill) if not skill.is_empty() else -1
|
||
if previous_slot >= 0:
|
||
equipped_skills[previous_slot] = ""
|
||
equipped_skills[slot] = skill
|
||
emit_changed()
|
||
return true
|
||
|
||
func get_spent_learning_points() -> int:
|
||
var spent := 0
|
||
for skill in SKILLS.IDS:
|
||
spent += get_skill_rank(skill)
|
||
return spent
|
||
|
||
func get_skill_reset_cost() -> int:
|
||
return BALANCE.SKILL_RESET_COST_PER_LEVEL * maxi(1, level)
|
||
|
||
func can_reset_skills() -> bool:
|
||
return false # Skills belong to the entire run; choices cannot be rerolled at a shop.
|
||
|
||
func reset_skills() -> bool:
|
||
return false
|
||
|
||
func can_train(skill: String) -> bool:
|
||
return learning_points > 0 and skill in skill_offers and get_skill_rank(skill) < SKILLS.MAX_RANK
|
||
|
||
func train_skill(skill: String) -> bool:
|
||
if not can_train(skill):
|
||
return false
|
||
skills[skill] = get_skill_rank(skill) + 1
|
||
if get_skill_rank(skill) == 1 and bool(SKILLS.DATA[skill]["active"]):
|
||
normalize_equipped_skills()
|
||
var empty := equipped_skills.find("")
|
||
if empty >= 0:
|
||
equipped_skills[empty] = skill
|
||
learning_points -= 1
|
||
skill_offers.clear()
|
||
prepare_skill_offers()
|
||
emit_changed()
|
||
return true
|
||
|
||
func prepare_skill_offers() -> void:
|
||
if learning_points <= 0 or not skill_offers.is_empty():
|
||
return
|
||
var pool: Array[String] = []
|
||
for skill in SKILLS.IDS:
|
||
if get_skill_rank(skill) < SKILLS.MAX_RANK:
|
||
pool.append(skill)
|
||
if pool.is_empty():
|
||
learning_points = 0
|
||
return
|
||
while skill_offers.size() < 3 and not pool.is_empty():
|
||
var index := _skill_rng.randi_range(0, pool.size() - 1)
|
||
skill_offers.append(pool[index])
|
||
pool.remove_at(index)
|
||
|
||
func get_luck_chance(equipment_bonus_percent: float = 0.0) -> float:
|
||
return clampf(get_skill_bonus("lucky") + equipment_bonus_percent / 100.0, 0.0, MAX_LUCK_CHANCE)
|
||
|
||
func roll_loot_level(base_level: int, roll: float = -1.0, equipment_bonus_percent: float = 0.0) -> int:
|
||
var level := clampi(base_level, 1, 1000)
|
||
var chance := get_luck_chance(equipment_bonus_percent)
|
||
if chance <= 0.0 or level >= 1000:
|
||
return level
|
||
var luck_roll := randf() if roll < 0.0 else roll
|
||
return mini(1000, level + 1) if luck_roll < chance else level
|
||
|
||
func discounted_price(base_price: int) -> int:
|
||
return maxi(1, roundi(base_price * (1.0 - get_skill_bonus("business_shark"))))
|
||
|
||
func should_consume_ammunition(ammo_kind: String) -> bool:
|
||
# Accumulate fractions for an exact saving rate instead of a lucky streak.
|
||
var credit := float(_ammo_credit.get(ammo_kind, 0.0)) + get_skill_bonus("efficient_magazine")
|
||
var saved := credit >= 1.0 - 0.000001
|
||
_ammo_credit[ammo_kind] = maxf(0.0, credit - 1.0) if saved else credit
|
||
return not saved
|
||
|
||
func reset_combat_state() -> void:
|
||
skill_cooldowns.clear()
|
||
skill_durations.clear()
|
||
emit_changed()
|
||
|
||
func tick_skills(delta: float) -> void:
|
||
for skill in skill_cooldowns:
|
||
skill_cooldowns[skill] = maxf(0.0, float(skill_cooldowns[skill]) - delta)
|
||
for skill in skill_durations:
|
||
skill_durations[skill] = maxf(0.0, float(skill_durations[skill]) - delta)
|
||
|
||
func is_skill_active(skill: String) -> bool:
|
||
return float(skill_durations.get(skill, 0.0)) > 0.0
|
||
|
||
func energy_regeneration_multiplier() -> float:
|
||
var bonus := 1.0
|
||
for skill in ["reactor_overload", "power_redirect"]:
|
||
if is_skill_active(skill):
|
||
bonus += get_skill_bonus(skill)
|
||
return bonus
|
||
|
||
func skill_unavailable_reason(skill: String, ship: CharacterBody2D) -> String:
|
||
if not SKILLS.DATA.has(skill) or not bool(SKILLS.DATA[skill]["active"]) or get_skill_rank(skill) <= 0:
|
||
return "Umiejętność nie jest odblokowana"
|
||
if skill not in equipped_skills:
|
||
return "Przypisz umiejętność do slotu F1–F5 w panelu kapitana"
|
||
if bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")):
|
||
return "Wymagany aktywny lot"
|
||
if float(skill_cooldowns.get(skill, 0.0)) > 0.0:
|
||
return "Odnowienie: %.1f s" % float(skill_cooldowns[skill])
|
||
if skill == "power_redirect" and (float(ship.get("max_shields")) <= 0.0 or float(ship.get("shields")) < float(ship.get("max_shields")) * 0.25):
|
||
return "Wymagane min. 25% tarcz"
|
||
return ""
|
||
|
||
func activate_skill(skill: String, ship: CharacterBody2D) -> bool:
|
||
if not skill_unavailable_reason(skill, ship).is_empty():
|
||
return false
|
||
skill_cooldowns[skill] = SKILLS.COOLDOWN
|
||
skill_durations[skill] = SKILLS.duration(skill)
|
||
if skill == "power_redirect":
|
||
ship.set("shields", 0.0)
|
||
ship.call("_update_shield_visual")
|
||
emit_changed()
|
||
return true
|
||
|
||
func add_rewards(xp_amount: int, credit_amount: int) -> void:
|
||
experience += maxi(0, xp_amount)
|
||
credits += maxi(0, credit_amount)
|
||
while experience >= get_xp_required():
|
||
experience -= get_xp_required()
|
||
level += 1
|
||
learning_points += BALANCE.LEARNING_POINTS_PER_LEVEL
|
||
prepare_skill_offers()
|
||
emit_changed()
|
||
|
||
func encounter_xp(base_amount: float, system_level: int, xp_multiplier: float = 1.0) -> int:
|
||
return maxi(0, roundi(maxf(0.0, base_amount) * maxf(0.0, xp_multiplier) * BALANCE.encounter_xp_factor(level, system_level)))
|
||
|
||
func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float, grant_credits: bool = true) -> Vector2i:
|
||
var index := clampi(tier, 0, 2)
|
||
var scale_factor := preload("res://scripts/levels/level_balance.gd").item_stat_scale(clampi(system_level, 1, 1000))
|
||
var xp := encounter_xp(BALANCE.KILL_XP[index] * scale_factor, system_level, xp_multiplier)
|
||
var money := roundi(BALANCE.KILL_CREDITS[index] * scale_factor * maxf(0.0, credit_multiplier))
|
||
add_rewards(xp, money if grant_credits else 0)
|
||
return Vector2i(xp, money)
|