256 lines
15 KiB
GDScript
256 lines
15 KiB
GDScript
extends Resource
|
||
class_name ItemInstance
|
||
|
||
const ENERGY_REGENERATION_BONUS_MULTIPLIER := 0.5
|
||
const EXTENSION_COOLDOWNS := preload("res://scripts/items/extension_cooldowns.gd")
|
||
# Exported with the item; old items default to 30 seconds without rerolling.
|
||
@export_range(5, 30, 1) var extension_cooldown: int = 30
|
||
var _instance_ability: ActiveAbility
|
||
@export var passive_value: float = -1.0
|
||
@export var special_orbs: Array[String] = []
|
||
@export var stack_count: int = 1
|
||
@export var artillery_balance_revision: int = 0
|
||
|
||
func shield_piercing() -> float:
|
||
return 0.5 if definition != null and definition.category == ItemDefinition.Category.WEAPON and "piercing" in special_orbs else 0.0
|
||
|
||
func roll_shield_piercing(roll: float = -1.0) -> float:
|
||
if definition != null and definition.weapon_effect == "pulse_laser":
|
||
if (randf() if roll < 0.0 else roll) < 0.5: return 1.0
|
||
return shield_piercing()
|
||
|
||
func weapon_damage_scale() -> float:
|
||
if definition == null: return 1.0
|
||
if definition.weapon_effect == "external_turret": return 0.5
|
||
if definition.weapon_effect == "pulse_laser": return 0.7
|
||
return 1.0
|
||
|
||
func get_damage_type() -> int:
|
||
if "electricity" in special_orbs: return DamageTypes.Type.ENERGY
|
||
if "radiation" in special_orbs: return DamageTypes.Type.RADIATION
|
||
if "kinetic" in special_orbs: return DamageTypes.Type.KINETIC
|
||
return definition.get_damage_type() if definition != null else DamageTypes.Type.KINETIC
|
||
|
||
func attack_speed_multiplier() -> float:
|
||
return 1.0 + clampf(get_stat_total("attack_speed_percent"), 0.0, 25.0) / 100.0
|
||
|
||
func get_fire_interval() -> float:
|
||
if definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE:
|
||
return maxf(1.0, maxf(1.0, 5.0 - get_stat_total("missile_cooldown_reduction")) / attack_speed_multiplier())
|
||
return maxf(definition.fire_interval, definition.weapon_cooldown) / attack_speed_multiplier()
|
||
|
||
func get_turret_fire_interval() -> float:
|
||
return definition.fire_interval / attack_speed_multiplier()
|
||
|
||
func is_passive_extension() -> bool:
|
||
return definition != null and definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and not definition.passive_effect.is_empty()
|
||
|
||
func get_passive_value() -> float:
|
||
if not is_passive_extension():
|
||
return 0.0
|
||
match definition.passive_effect:
|
||
"recycler": return clampf(passive_value, 0.5, 40.0)
|
||
"virus": return clampf(floorf(passive_value), 1.0, 20.0)
|
||
"titanium_armor": return clampf(passive_value, 5.0, 50.0)
|
||
return 0.0
|
||
|
||
func get_passive_description() -> String:
|
||
var value := get_passive_value()
|
||
match definition.passive_effect:
|
||
"recycler": return "Każdy zebrany odłamek asteroidy odnawia %.1f%% maksymalnych tarcz i energii." % value
|
||
"virus": return "Ma 50%% szans na przejęcie uciekającego wroga (poniżej 10%% kadłuba). Jedna próba na statek; losowanie następuje, gdy jest wolne miejsce na przejęcie. Zamiast uciekać atakują swoich dawnych sojuszników. Bossowie są odporni na wirusa. Wrogowie atakują zawirusowane statki, gdy gracza nie ma w pobliżu. Nadal możesz uszkodzić i zniszczyć je własną bronią. Limit jednocześnie kontrolowanych: %d." % int(value)
|
||
"titanium_armor": return "Pochłania 50%% otrzymywanych obrażeń, gdy kadłub ma nie więcej niż %d%% maksimum. Próg sprawdzany przed trafieniem." % int(value)
|
||
return ""
|
||
|
||
|
||
func get_active_ability() -> ActiveAbility:
|
||
if definition == null or definition.active_ability == null:
|
||
return null
|
||
if definition.category != ItemDefinition.Category.ACTIVE_EXTENSION:
|
||
return definition.active_ability
|
||
if _instance_ability != null and _instance_ability.cooldown == clampi(extension_cooldown, 5, 30):
|
||
return _instance_ability
|
||
if _instance_ability == null:
|
||
# Never mutate the shared ability resource in the item definition.
|
||
_instance_ability = definition.active_ability.duplicate() as ActiveAbility
|
||
_instance_ability.cooldown = clampi(extension_cooldown, 5, 30)
|
||
var description := definition.active_ability.description
|
||
var fixed_cooldown := RegEx.new()
|
||
fixed_cooldown.compile(" (Odnowienie:|Zrzut co) [0-9]+ s\\.")
|
||
description = fixed_cooldown.sub(description, "", true)
|
||
if definition.item_id == "mine_layer":
|
||
description = "Zostawia minę na 60 s. Uzbraja się po 0,7 s. Wykrywa bliski przelot wroga. Wybuch w promieniu 140 jednostek zadaje każdemu wrogowi 50% aktualnego kadłuba bez redukcji absorpcji (tarcze pochłaniają obrażenia) i spowalnia o 50% na 5 s."
|
||
if definition.item_id == "afterburner":
|
||
description = "Szybko rozpędza do 300% zwykłej prędkości (+200%), następnie wytraca nadmiar prędkości. Bez kosztu energii."
|
||
_instance_ability.description = description + "\nOdnowienie: %d s." % _instance_ability.cooldown
|
||
return _instance_ability
|
||
|
||
@export var definition: ItemDefinition
|
||
@export_range(1, 1000, 1) var item_level: int = 1
|
||
@export var additional_modifiers: Array[ItemStatModifier] = []
|
||
@export var primary_value_override: float = -1.0
|
||
@export var energy_regeneration_override: float = -1.0
|
||
@export var shield_regeneration_override: float = -1.0
|
||
@export_range(0, 4) var rarity: int = 0
|
||
@export var damage_min: float = -1.0
|
||
@export var damage_max: float = -1.0
|
||
|
||
func get_stat_total(stat_key: String) -> float:
|
||
if stat_key == "damage" and definition and definition.category == ItemDefinition.Category.WEAPON:
|
||
var damage_range := get_damage_range()
|
||
return (damage_range.x + damage_range.y) * 0.5
|
||
var total := 0.0
|
||
if definition and definition.primary_stat == stat_key:
|
||
total += primary_value_override if primary_value_override >= 0.0 else definition.primary_value
|
||
if definition and stat_key == "energy_regeneration":
|
||
total += energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus
|
||
if definition and stat_key == "shield_regeneration":
|
||
total += get_base_shield_regeneration()
|
||
for modifier in additional_modifiers:
|
||
if modifier and modifier.stat_key == stat_key:
|
||
total += modifier.value
|
||
return total * ENERGY_REGENERATION_BONUS_MULTIPLIER if stat_key == "energy_regeneration" else total
|
||
|
||
func get_base_shield_regeneration() -> float:
|
||
if definition == null: return 0.0
|
||
if shield_regeneration_override >= 0.0: return shield_regeneration_override
|
||
# Previously acquired modules gain the new base property at their current level.
|
||
return definition.shield_regeneration_bonus * preload("res://scripts/levels/level_balance.gd").item_stat_scale(item_level)
|
||
|
||
func get_damage_range() -> Vector2:
|
||
if definition == null or definition.category != ItemDefinition.Category.WEAPON:
|
||
return Vector2.ZERO
|
||
var lower := damage_min
|
||
var upper := damage_max
|
||
if lower < 0.0 or upper < 0.0:
|
||
if primary_value_override >= 0.0:
|
||
# Older item instances retain their previously rolled damage.
|
||
lower = primary_value_override
|
||
upper = primary_value_override
|
||
else:
|
||
# Starter weapons use the middle of the definition's roll settings.
|
||
lower = roundf((definition.damage_lower_roll_min + definition.damage_lower_roll_max) * 0.5)
|
||
upper = lower + roundf((definition.damage_spread_roll_min + definition.damage_spread_roll_max) * 0.5)
|
||
if definition.weapon_effect == "artillery" and artillery_balance_revision == 0 and (damage_min >= 0.0 or primary_value_override >= 0.0):
|
||
lower *= 50.0/74.0
|
||
upper *= 50.0/74.0
|
||
var flat_bonus := 0.0
|
||
for modifier in additional_modifiers:
|
||
if modifier and modifier.stat_key == "damage":
|
||
flat_bonus += modifier.value
|
||
var result := Vector2(maxf(0.0, minf(lower, upper) + flat_bonus), maxf(0.0, maxf(lower, upper) + flat_bonus))
|
||
return result * weapon_damage_scale()
|
||
|
||
func roll_damage() -> float:
|
||
var damage_range := get_damage_range()
|
||
var damage_scale := weapon_damage_scale()
|
||
if damage_scale != 1.0:
|
||
return damage_range.x + randi_range(0, roundi((damage_range.y-damage_range.x)/damage_scale)) * damage_scale
|
||
# Inclusive integer rolls, preserving fractional flat bonuses on both bounds.
|
||
var whole_span := floori(damage_range.y - damage_range.x)
|
||
return damage_range.x + randi_range(0, whole_span)
|
||
|
||
func get_energy_cost() -> float:
|
||
if definition == null or definition.category != ItemDefinition.Category.WEAPON:
|
||
return 0.0
|
||
if definition.weapon_kind in [ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.MISSILE, ItemDefinition.WeaponKind.GAMMA]:
|
||
return 0.0
|
||
# Compare this weapon with its starter damage. Captain/global gear buffs are free.
|
||
var base_lower := roundf((definition.damage_lower_roll_min + definition.damage_lower_roll_max) * 0.5)
|
||
var base_spread := roundf((definition.damage_spread_roll_min + definition.damage_spread_roll_max) * 0.5)
|
||
var base_average := maxf(1.0, base_lower + base_spread * 0.5)
|
||
base_average *= weapon_damage_scale()
|
||
var damage_range := get_damage_range()
|
||
var average := maxf(0.1, (damage_range.x + damage_range.y) * 0.5)
|
||
var damage_factor := pow(average / base_average, definition.energy_damage_exponent)
|
||
var addon_factor := 1.0 + get_addon_count() * definition.energy_per_addon
|
||
if definition.weapon_effect == "pulse_laser":
|
||
# Apply the surcharge after the ordinary laser's rounding, preserving +30%.
|
||
return snappedf(maxf(0.0,definition.energy_cost/1.3*damage_factor*addon_factor),0.1)*1.3
|
||
return snappedf(maxf(0.0, definition.energy_cost * damage_factor * addon_factor), 0.1)
|
||
|
||
func get_damage_range_text() -> String:
|
||
var damage_range := get_damage_range()
|
||
return "%s–%s DMG" % [_format_damage(damage_range.x), _format_damage(damage_range.y)]
|
||
|
||
static func _format_damage(value: float) -> String:
|
||
return str(roundi(value)) if is_equal_approx(value, roundf(value)) else "%.1f" % value
|
||
|
||
func get_rarity_name() -> String:
|
||
return ["Zwykły", "Niepospolity", "Rzadki", "Epicki", "Legendarny"][clampi(rarity, 0, 4)]
|
||
|
||
func get_rarity_color() -> Color:
|
||
return get_background_color().lightened(0.4)
|
||
|
||
func get_addon_count() -> int:
|
||
var count := 0
|
||
for modifier in additional_modifiers:
|
||
if modifier != null:
|
||
count += 1
|
||
return count
|
||
|
||
func get_background_color() -> Color:
|
||
# No addons and one addon are black; the strongest tier is burgundy.
|
||
return [Color("080b10"), Color("080b10"), Color("603e28"), Color("285b91"), Color("8050b8"), Color("70243c")][clampi(get_addon_count(), 0, 5)]
|
||
|
||
func get_type_color() -> Color:
|
||
if definition == null:
|
||
return Color("354455")
|
||
match definition.category:
|
||
ItemDefinition.Category.WEAPON:
|
||
return Color("ae74ef")
|
||
ItemDefinition.Category.ACTIVE_EXTENSION:
|
||
return Color("85e5b6") if is_passive_extension() else Color("f39c4d")
|
||
return Color("65ce8b")
|
||
|
||
func get_tooltip_text() -> String:
|
||
if definition == null:
|
||
return ""
|
||
if definition.category == ItemDefinition.Category.CONSUMABLE:
|
||
if not definition.orb_id.is_empty():
|
||
return "%s\nStos: %d\n\n%s\n\nDwuklik: wybierz orb. Następnie kliknij odpowiedni moduł lub broń. PPM lub Esc anuluje." % [definition.display_name,stack_count,definition.description]
|
||
return "%s\n\n%s\nR • Zużywa jedną sztukę z zasobów." % [definition.display_name, definition.description]
|
||
var primary_text := "%s: +%.1f%s" % [stat_name(definition.primary_stat), get_stat_total(definition.primary_stat), definition.stat_suffix]
|
||
if definition.category == ItemDefinition.Category.WEAPON:
|
||
primary_text = "Obrażenia: %s" % get_damage_range_text()
|
||
elif is_passive_extension():
|
||
primary_text = get_passive_description() + "\nPASYWNE • Bez kosztu i cooldownu.\nDuplikaty nie sumują efektu — działa najlepszy egzemplarz."
|
||
elif definition.active_ability != null:
|
||
primary_text = get_active_ability().description
|
||
var lines: Array[String] = [definition.display_name, "%s • Poziom %d" % [get_rarity_name(), item_level], "", definition.description, "", "CECHA GŁÓWNA", primary_text]
|
||
if definition.energy_regeneration_bonus > 0.0:
|
||
lines.append("Regeneracja generatora: +%.1f /s" % ((energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus) * ENERGY_REGENERATION_BONUS_MULTIPLIER))
|
||
if definition.shield_regeneration_bonus > 0.0:
|
||
lines.append("Regeneracja tarcz: +%.1f pkt/s" % get_base_shield_regeneration())
|
||
if definition.category == ItemDefinition.Category.WEAPON:
|
||
lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[get_damage_type()])
|
||
if definition.weapon_effect == "pulse_laser": lines.append("Prędkość lecącej wiązki: %.0f px/s" % definition.projectile_speed)
|
||
lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.")
|
||
if definition.weapon_effect == "external_turret":
|
||
lines.append("Zasięg działka: %.0f px • Magazynek: 50 pocisków • Koszt rozstawienia: 50 amunicji" % definition.weapon_range)
|
||
lines.append("Odstęp strzałów działka: %.2f s • Kolejne rozstawienie: %.2f s" % [get_turret_fire_interval(),get_fire_interval()])
|
||
elif definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN:
|
||
lines.append("Zasięg: %.0f px • Amunicja: %d / strzał" % [definition.weapon_range,definition.ammunition_cost])
|
||
elif definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA:
|
||
lines.append("Zasięg: %.0f px • Napromieniowana amunicja: 1 / strzał" % definition.weapon_range)
|
||
elif definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE:
|
||
lines.append("Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii")
|
||
else:
|
||
lines.append("Zasięg: %.0f px • Energia: %.1f / strzał" % [definition.weapon_range, get_energy_cost()])
|
||
if definition.weapon_effect != "external_turret": lines.append("Odstęp strzałów: %.2f s" % get_fire_interval())
|
||
if not additional_modifiers.is_empty():
|
||
lines.append("\nADDONY (aktywne po zamontowaniu)")
|
||
for modifier in additional_modifiers:
|
||
if modifier == null:
|
||
continue
|
||
lines.append(modifier.get_display_text())
|
||
for orb in special_orbs:
|
||
if orb == "piercing": lines.append("\nORB PRZEBICIA · 50% obrażeń omija tarcze. Tylko ta broń.")
|
||
elif orb == "booster": lines.append("\nORB DOPALACZA · TAB bez kosztu energii. Działa po zamontowaniu silnika.")
|
||
elif orb in ["electricity", "radiation", "kinetic", "atomic_engine", "polarization"]:
|
||
lines.append("\n%s · %s" % [preload("res://scripts/items/special_orbs.gd").NAMES[orb], preload("res://scripts/items/special_orbs.gd").DESCRIPTIONS[orb]])
|
||
return "\n".join(lines)
|
||
|
||
static func stat_name(key: String) -> String:
|
||
return str({"attack_speed_percent":"Szybkostrzelność", "luck_percent": "Szczęście", "kinetic_absorption": "Absorpcja kinetyczna", "energy_absorption": "Absorpcja energetyczna", "radiation_absorption": "Absorpcja radiacyjna", "missile_cooldown_reduction": "Skrócenie cooldownu rakiet", "scanner_range": "Zasięg skanera", "shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key))
|