Randoloot/scripts/items/item_instance.gd
2026-10-17 16:20:14 +02:00

189 lines
10 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
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 "Przejmuje uciekających wrogów (poniżej 10%% kadłuba). Zamiast uciekać atakują swoich dawnych sojuszników. 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_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
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_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)
var flat_bonus := 0.0
for modifier in additional_modifiers:
if modifier and modifier.stat_key == "damage":
flat_bonus += modifier.value
return Vector2(maxf(0.0, minf(lower, upper) + flat_bonus), maxf(0.0, maxf(lower, upper) + flat_bonus))
func roll_damage() -> float:
var damage_range := get_damage_range()
# 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)
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
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:
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.category == ItemDefinition.Category.WEAPON:
lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[definition.get_damage_type()])
lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.")
if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN:
lines.append("Zasięg: %.0f px • Amunicja: 1 / strzał" % definition.weapon_range)
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()])
lines.append("Odstęp strzałów: %.2f s" % maxf(definition.fire_interval, definition.weapon_cooldown))
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())
return "\n".join(lines)
static func stat_name(key: String) -> String:
return str({"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))