System itemów
This commit is contained in:
parent
d7099548b5
commit
e5067bb0e2
92
documentation.txt
Normal file
92
documentation.txt
Normal file
@ -0,0 +1,92 @@
|
||||
SYSTEM PRZEDMIOTÓW I LOOTU
|
||||
|
||||
Przedmioty: Weapon, Shield, Hull, Generator, Extension. Baza typów znajduje się
|
||||
w resources/items, generator w scripts/items/item_generator.gd.
|
||||
Poziom przedmiotu = GameState.system_level (1–1000; obecnie układ 1).
|
||||
Cecha główna modułów i regeneracja generatora: baza * (1 + 0.05 * (L - 1)) * losowe 0.8–1.2.
|
||||
Definicje bazowe pozostają wspólne i nie są zmieniane; wylosowane wartości zapisuje
|
||||
ItemInstance. Każdy drop ma 1–5 różnych addonów z bazy zasobów affix_database.tres.
|
||||
Liczba addonów zwykłego dropu: 1=50%, 2=25%, 3=14%, 4=8%, 5=3%.
|
||||
Elite dostaje jeden addon więcej (maks. 5), Boss co najmniej 4.
|
||||
Rzadkość wg liczby addonów: zwykły, niepospolity, rzadki, epicki, legendarny.
|
||||
Zakresy addonów interpolują liniowo między poziomem 1 a 1000 wg concept_game.txt.
|
||||
Cap dotyczy pojedynczego addonu; bonusy zamontowanych przedmiotów sumują się.
|
||||
|
||||
Po zniszczeniu wroga: Normal 65%, Elite 90%, Boss 100% szansy na jeden przedmiot.
|
||||
Typ wroga ustawia eksportowane loot_tier; obecna mapa nadal tworzy zwykłych wrogów.
|
||||
Drop pozostaje na mapie, przyciąga się z 150 px i zbiera do wolnego slotu z 48 px.
|
||||
Pełna ładownia: drop czeka i pokazuje komunikat. Zniszczony gracz nie zbiera lootu.
|
||||
Niezebrany loot znika przy restarcie poziomu. Zebrane przedmioty zachowują wszystkie
|
||||
wylosowane wartości, addony, poziomy i rozmieszczenie po śmierci gracza.
|
||||
|
||||
Addony działają wyłącznie w zamontowanej broni/modułach. Procenty kadłuba, tarcz
|
||||
i prędkości mnożą bazę powiększoną o bonusy płaskie. Bonus obrażeń działa na obie
|
||||
bronie (także laser i rakiety). Regeneracja dodaje punkty na sekundę.
|
||||
EXP i kredyty mają obliczone mnożniki statku; naliczanie nagród wymaga przyszłego
|
||||
systemu postępu pilota i waluty, których gra obecnie nie posiada.
|
||||
|
||||
UI: kolor przedmiotu wskazuje rzadkość. Najechanie na slot pokazuje pełne parametry,
|
||||
poziom, rzadkość, addony i parametry broni. Przenoszenie nadal odbywa się drag/drop.
|
||||
Małe × w zajętym slocie magazynu usuwa przedmiot natychmiast, bez przesuwania
|
||||
pozostałych slotów. Przedmioty zamontowane nie mają przycisku usuwania.
|
||||
|
||||
EDYCJA BAZY CECH (ADDONÓW)
|
||||
|
||||
Katalog bazy: resources/items/affix_database.tres.
|
||||
Definicje poszczególnych cech: resources/items/affixes/*.tres.
|
||||
Klasa pól i reguł: scripts/items/item_affix_definition.gd.
|
||||
Losowanie ważone i kontrola zgodności: scripts/items/item_affix_database.gd.
|
||||
|
||||
W Godocie wybierz plik cechy .tres w FileSystem i edytuj go w Inspectorze:
|
||||
- affix_id: unikalny identyfikator cechy.
|
||||
- display_name, description, unit: nazwa, opis, jednostka.
|
||||
- stat_key: identyfikator efektu obsługiwanego przez statek/bronie.
|
||||
- enabled: włącza lub wyłącza przyszłe losowanie cechy.
|
||||
- selection_weight: względna waga losowania (2 = dwa razy większa waga niż 1).
|
||||
Waga 0 wyłącza losowanie; procent szansy zależy od pozostałej dozwolonej puli.
|
||||
- category_mask: zaznacz dozwolone Weapon, Shield, Hull, Generator, Extension.
|
||||
Obecnie zaznaczone są wszystkie kategorie, zgodnie z wcześniejszą pulą ogólną.
|
||||
- allowed_item_ids: pusta lista dopuszcza wszystkie itemy dozwolonych kategorii;
|
||||
wpisana lista dopuszcza tylko podane item_id z bazowych resources/items/*.tres.
|
||||
- excluded_item_ids: zawsze wyklucza wskazane itemy.
|
||||
- minimum_level / maximum_level: poziomy, na których cecha może się pojawić.
|
||||
- minimum/maximum_value_level_1 i level_1000: zakresy dla poziomów krańcowych;
|
||||
między nimi generator interpoluje liniowo. Dla stałej wartości ustaw wszystkie
|
||||
cztery pola identycznie.
|
||||
- exclusive_group: puste = bez ograniczenia. Cechy o tej samej niepustej grupie
|
||||
nie mogą pojawić się razem na jednym przedmiocie.
|
||||
- incompatible_affix_ids: lista cech wykluczonych na tym samym przedmiocie.
|
||||
Wystarczy wpisać wykluczenie po jednej stronie. Ta sama cecha nie powtarza się.
|
||||
|
||||
Aby dodać cechę: utwórz zasób ItemAffixDefinition, zapisz w affixes i dodaj do
|
||||
tablicy affixes w affix_database.tres. Sama obecność pliku w katalogu nie dodaje
|
||||
go do losowania. Aktualne osiem cech ma wagę 1 i nie ma wzajemnych wykluczeń.
|
||||
Nowy stat_key wymaga również implementacji efektu w kodzie; baza opisuje reguły
|
||||
losowania i prezentację, nie tworzy automatycznie nowych mechanik statku.
|
||||
Tooltip pobiera nazwę i jednostkę z definicji cechy. Wylosowana wartość pozostaje
|
||||
w ItemStatModifier; zmiana zakresów w bazie nie przelicza zdobytych przedmiotów.
|
||||
Przy zbyt małej puli zgodnych cech generator kończy losowanie wcześniej, bez
|
||||
duplikatów i bez pętli nieskończonej. Rzadkość wynika z faktycznej liczby addonów.
|
||||
Pusta dozwolona pula daje przedmiot bez addonów o rzadkości zwykłej.
|
||||
|
||||
ZAKRES OBRAŻEŃ BRONI
|
||||
|
||||
Każda nowa broń ma zapisane w ItemInstance damage_min / damage_max. Generator
|
||||
losuje dolną granicę oraz szerokość zakresu. Obie granice skaluje przez
|
||||
(1 + 0.05 * (L - 1)) i zaokrągla do całkowitych punktów obrażeń.
|
||||
Wartości dla poziomu 1 edytuje się w zasobach poszczególnych broni .tres:
|
||||
damage_lower_roll_min/max = zakres losowania dolnej granicy;
|
||||
damage_spread_roll_min/max = zakres losowania szerokości (górna = dolna + szerokość).
|
||||
Laser: dolna 1–4, szerokość 2–4 (możliwe m.in. 1–5 i 4–6).
|
||||
Plazma: dolna 10–14, szerokość 2–4.
|
||||
Działko maszynowe: dolna 2–4, szerokość 1–2.
|
||||
Rakiety: dolna 45–58, szerokość 5–12.
|
||||
|
||||
Każdy wystrzelony pocisk i każdy trafiający impuls lasera losują obrażenia
|
||||
równomiernie z zakresu swojej broni, włącznie z obiema granicami.
|
||||
Po losowaniu naliczany jest mnożnik damage_percent z całego wyposażenia.
|
||||
Jedna rakieta ma jeden wynik losowania, wspólny dla trafienia i jej eksplozji.
|
||||
Tooltip i etykieta slotu pokazują bazowy zakres broni; bonus procentowy statku
|
||||
jest naliczany w walce. Zakres dropu zachowuje się po restarcie poziomu.
|
||||
Bronie startowe mają zakres z wartości środkowych ustawień w definicji.
|
||||
Starszy item z primary_value_override i bez zakresu zachowuje stałe obrażenia.
|
||||
17
resources/items/affix_database.tres
Normal file
17
resources/items/affix_database.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=11 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_database.gd" id="1_database"]
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="2_type"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/hull_percent.tres" id="3_affix"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/damage_percent.tres" id="4_affix"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/shield_percent.tres" id="5_affix"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/energy_regeneration.tres" id="6_affix"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/shield_regeneration.tres" id="7_affix"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/speed_percent.tres" id="8_affix"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/experience_percent.tres" id="9_affix"]
|
||||
[ext_resource type="Resource" path="res://resources/items/affixes/credits_percent.tres" id="10_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_database")
|
||||
affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix")])
|
||||
|
||||
17
resources/items/affixes/credits_percent.tres
Normal file
17
resources/items/affixes/credits_percent.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "credits_percent"
|
||||
display_name = "Kredyty"
|
||||
stat_key = "credits_percent"
|
||||
unit = "%"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 2
|
||||
maximum_value_level_1 = 5
|
||||
minimum_value_level_1000 = 40
|
||||
maximum_value_level_1000 = 75
|
||||
|
||||
17
resources/items/affixes/damage_percent.tres
Normal file
17
resources/items/affixes/damage_percent.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "damage_percent"
|
||||
display_name = "Obrażenia broni"
|
||||
stat_key = "damage_percent"
|
||||
unit = "%"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 1
|
||||
maximum_value_level_1 = 3
|
||||
minimum_value_level_1000 = 25
|
||||
maximum_value_level_1000 = 50
|
||||
|
||||
17
resources/items/affixes/energy_regeneration.tres
Normal file
17
resources/items/affixes/energy_regeneration.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "energy_regeneration"
|
||||
display_name = "Regeneracja energii"
|
||||
stat_key = "energy_regeneration"
|
||||
unit = "/s"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 0.2
|
||||
maximum_value_level_1 = 0.5
|
||||
minimum_value_level_1000 = 100
|
||||
maximum_value_level_1000 = 180
|
||||
|
||||
17
resources/items/affixes/experience_percent.tres
Normal file
17
resources/items/affixes/experience_percent.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "experience_percent"
|
||||
display_name = "Doświadczenie (EXP)"
|
||||
stat_key = "experience_percent"
|
||||
unit = "%"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 2
|
||||
maximum_value_level_1 = 5
|
||||
minimum_value_level_1000 = 40
|
||||
maximum_value_level_1000 = 75
|
||||
|
||||
17
resources/items/affixes/hull_percent.tres
Normal file
17
resources/items/affixes/hull_percent.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "hull_percent"
|
||||
display_name = "Kadłub"
|
||||
stat_key = "hull_percent"
|
||||
unit = "%"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 1
|
||||
maximum_value_level_1 = 3
|
||||
minimum_value_level_1000 = 25
|
||||
maximum_value_level_1000 = 50
|
||||
|
||||
17
resources/items/affixes/shield_percent.tres
Normal file
17
resources/items/affixes/shield_percent.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "shield_percent"
|
||||
display_name = "Tarcze"
|
||||
stat_key = "shield_percent"
|
||||
unit = "%"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 1
|
||||
maximum_value_level_1 = 3
|
||||
minimum_value_level_1000 = 25
|
||||
maximum_value_level_1000 = 50
|
||||
|
||||
17
resources/items/affixes/shield_regeneration.tres
Normal file
17
resources/items/affixes/shield_regeneration.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "shield_regeneration"
|
||||
display_name = "Regeneracja tarcz"
|
||||
stat_key = "shield_regeneration"
|
||||
unit = "/s"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 0.5
|
||||
maximum_value_level_1 = 1
|
||||
minimum_value_level_1000 = 200
|
||||
maximum_value_level_1000 = 350
|
||||
|
||||
17
resources/items/affixes/speed_percent.tres
Normal file
17
resources/items/affixes/speed_percent.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1_affix"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_affix")
|
||||
affix_id = "speed_percent"
|
||||
display_name = "Prędkość statku"
|
||||
stat_key = "speed_percent"
|
||||
unit = "%"
|
||||
selection_weight = 1.0
|
||||
category_mask = 31
|
||||
minimum_value_level_1 = 1
|
||||
maximum_value_level_1 = 2
|
||||
minimum_value_level_1000 = 15
|
||||
maximum_value_level_1000 = 25
|
||||
|
||||
@ -15,4 +15,8 @@ weapon_kind = 2
|
||||
fire_interval = 0.2
|
||||
weapon_range = 800.0
|
||||
energy_cost = 12.0
|
||||
damage_lower_roll_min = 1
|
||||
damage_lower_roll_max = 4
|
||||
damage_spread_roll_min = 2
|
||||
damage_spread_roll_max = 4
|
||||
laser_pulse_duration = 0.08
|
||||
|
||||
@ -16,3 +16,7 @@ fire_interval = 0.1
|
||||
projectile_speed = 1150.0
|
||||
weapon_range = 550.0
|
||||
energy_cost = 3.0
|
||||
damage_lower_roll_min = 2
|
||||
damage_lower_roll_max = 4
|
||||
damage_spread_roll_min = 1
|
||||
damage_spread_roll_max = 2
|
||||
|
||||
@ -17,3 +17,7 @@ weapon_cooldown = 5.0
|
||||
projectile_speed = 400.0
|
||||
weapon_range = 1000.0
|
||||
energy_cost = 25.0
|
||||
damage_lower_roll_min = 45
|
||||
damage_lower_roll_max = 58
|
||||
damage_spread_roll_min = 5
|
||||
damage_spread_roll_max = 12
|
||||
|
||||
@ -16,3 +16,7 @@ fire_interval = 0.2857
|
||||
projectile_speed = 720.0
|
||||
weapon_range = 350.0
|
||||
energy_cost = 4.0
|
||||
damage_lower_roll_min = 10
|
||||
damage_lower_roll_max = 14
|
||||
damage_spread_roll_min = 2
|
||||
damage_spread_roll_max = 4
|
||||
|
||||
@ -4,6 +4,10 @@ const DAMAGE_POPUP_SCENE := preload("res://scenes/game/damage_popup.tscn")
|
||||
const IMPACT_EFFECT_SCENE := preload("res://scenes/game/impact_effect.tscn")
|
||||
const PLASMA_PROJECTILE_SCENE := preload("res://scenes/game/enemy_plasma_projectile.tscn")
|
||||
const WEAPON_AUDIO_SCRIPT := preload("res://scripts/game/weapon_audio.gd")
|
||||
const ITEM_GENERATOR := preload("res://scripts/items/item_generator.gd")
|
||||
const LOOT_PICKUP := preload("res://scripts/items/loot_pickup.gd")
|
||||
|
||||
@export_enum("Normal", "Elite", "Boss") var loot_tier: int = 0
|
||||
|
||||
@export var max_hull := 50.0
|
||||
@export var activation_range := 760.0
|
||||
@ -70,12 +74,25 @@ func _fire_plasma(direction: Vector2) -> void:
|
||||
WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, ItemDefinition.WeaponKind.PLASMA, -10.0)
|
||||
|
||||
func take_damage(amount: float) -> void:
|
||||
if is_queued_for_deletion() or hull <= 0.0:
|
||||
return
|
||||
hull = maxf(0.0, hull - amount)
|
||||
_show_damage_popup(amount)
|
||||
if hull <= 0.0:
|
||||
_drop_loot()
|
||||
_spawn_destruction_effect()
|
||||
queue_free()
|
||||
|
||||
func _drop_loot() -> void:
|
||||
var chance := 0.65 if loot_tier == 0 else (0.9 if loot_tier == 1 else 1.0)
|
||||
if randf() > chance:
|
||||
return
|
||||
var pickup := Node2D.new()
|
||||
pickup.set_script(LOOT_PICKUP)
|
||||
pickup.set("item", ITEM_GENERATOR.generate(GameState.system_level, loot_tier))
|
||||
pickup.position = position
|
||||
get_parent().call_deferred("add_child", pickup)
|
||||
|
||||
func _spawn_destruction_effect() -> void:
|
||||
var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D
|
||||
effect.global_position = global_position
|
||||
|
||||
@ -2,6 +2,7 @@ extends Node
|
||||
|
||||
var selected_ship_path: String = ""
|
||||
var restart_inventory: Dictionary = {}
|
||||
var system_level: int = 1
|
||||
|
||||
func _ready() -> void:
|
||||
var game_window := get_tree().root
|
||||
|
||||
@ -27,6 +27,9 @@ var shields := 100.0
|
||||
var energy := 100.0
|
||||
var energy_regeneration := 12.0
|
||||
var shield_regeneration := 0.0
|
||||
var damage_multiplier := 1.0
|
||||
var experience_multiplier := 1.0
|
||||
var credits_multiplier := 1.0
|
||||
|
||||
var angular_velocity := 0.0
|
||||
var _sprite: Sprite2D
|
||||
@ -35,10 +38,13 @@ var _is_destroyed := false
|
||||
@onready var _shield_visual: Node2D = $ShieldVisual
|
||||
|
||||
func apply_equipment_bonuses(bonuses: Dictionary) -> void:
|
||||
max_hull = base_max_hull + float(bonuses.get("max_hull", 0.0))
|
||||
max_shields = base_max_shields + float(bonuses.get("max_shields", 0.0))
|
||||
max_hull = (base_max_hull + float(bonuses.get("max_hull", 0.0))) * (1.0 + float(bonuses.get("hull_percent", 0.0)) / 100.0)
|
||||
max_shields = (base_max_shields + float(bonuses.get("max_shields", 0.0))) * (1.0 + float(bonuses.get("shield_percent", 0.0)) / 100.0)
|
||||
max_energy = base_max_energy + float(bonuses.get("max_energy", 0.0))
|
||||
max_speed = base_max_speed + float(bonuses.get("max_speed", 0.0))
|
||||
max_speed = (base_max_speed + float(bonuses.get("max_speed", 0.0))) * (1.0 + float(bonuses.get("speed_percent", 0.0)) / 100.0)
|
||||
damage_multiplier = 1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0
|
||||
experience_multiplier = 1.0 + float(bonuses.get("experience_percent", 0.0)) / 100.0
|
||||
credits_multiplier = 1.0 + float(bonuses.get("credits_percent", 0.0)) / 100.0
|
||||
energy_regeneration = base_energy_regeneration + float(bonuses.get("energy_regeneration", 0.0))
|
||||
shield_regeneration = base_shield_regeneration + float(bonuses.get("shield_regeneration", 0.0))
|
||||
reverse_max_speed = max_speed * 0.5
|
||||
|
||||
@ -49,6 +49,10 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
_fire_just_pressed[slot_index] = true
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if bool(ship.get("_is_destroyed")):
|
||||
for line in _laser_lines:
|
||||
line.visible = false
|
||||
return
|
||||
var mouse_over_ui := ship.get_viewport().gui_get_hovered_control() != null
|
||||
for slot_index in 2:
|
||||
_shot_timers[slot_index] = maxf(0.0, _shot_timers[slot_index] - delta)
|
||||
@ -77,23 +81,24 @@ func _physics_process(delta: float) -> void:
|
||||
match definition.weapon_kind:
|
||||
ItemDefinition.WeaponKind.LASER:
|
||||
_laser_lines[slot_index].visible = _laser_pulse_timers[slot_index] > 0.0
|
||||
_fire_laser(slot_index, definition, item.get_stat_total("damage"))
|
||||
_fire_laser(slot_index, item)
|
||||
ItemDefinition.WeaponKind.MISSILE:
|
||||
if (
|
||||
is_just_pressed
|
||||
and _shot_timers[slot_index] <= 0.0
|
||||
and _consume_energy(definition.energy_cost)
|
||||
):
|
||||
_fire_projectile(definition, item.get_stat_total("damage"))
|
||||
_fire_projectile(item)
|
||||
_shot_timers[slot_index] = definition.weapon_cooldown
|
||||
_:
|
||||
if _shot_timers[slot_index] <= 0.0 and _consume_energy(definition.energy_cost):
|
||||
_fire_projectile(definition, item.get_stat_total("damage"))
|
||||
_fire_projectile(item)
|
||||
_shot_timers[slot_index] = definition.fire_interval
|
||||
for slot_index in 2:
|
||||
_fire_just_pressed[slot_index] = false
|
||||
|
||||
func _fire_projectile(definition: ItemDefinition, projectile_damage: float) -> void:
|
||||
func _fire_projectile(item: ItemInstance) -> void:
|
||||
var definition := item.definition
|
||||
var muzzle_position := _muzzle_position()
|
||||
var direction := _aim_direction_from(muzzle_position)
|
||||
var projectile := PROJECTILE_SCENE.instantiate()
|
||||
@ -103,13 +108,14 @@ func _fire_projectile(definition: ItemDefinition, projectile_damage: float) -> v
|
||||
direction,
|
||||
definition.weapon_kind,
|
||||
definition.projectile_speed,
|
||||
projectile_damage,
|
||||
item.roll_damage() * float(ship.get("damage_multiplier")),
|
||||
definition.weapon_range
|
||||
)
|
||||
ship.get_parent().add_child(projectile)
|
||||
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind)
|
||||
|
||||
func _fire_laser(slot_index: int, definition: ItemDefinition, beam_damage: float) -> void:
|
||||
func _fire_laser(slot_index: int, item: ItemInstance) -> void:
|
||||
var definition := item.definition
|
||||
var line := _laser_lines[slot_index]
|
||||
if _shot_timers[slot_index] > 0.0 or float(ship.get("energy")) < definition.energy_cost:
|
||||
return
|
||||
@ -134,7 +140,7 @@ func _fire_laser(slot_index: int, definition: ItemDefinition, beam_damage: float
|
||||
return
|
||||
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, ItemDefinition.WeaponKind.LASER)
|
||||
if not result.is_empty():
|
||||
_apply_damage(result.get("collider"), beam_damage)
|
||||
_apply_damage(result.get("collider"), item.roll_damage() * float(ship.get("damage_multiplier")))
|
||||
_spawn_laser_impact(endpoint)
|
||||
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration
|
||||
_shot_timers[slot_index] = definition.fire_interval
|
||||
|
||||
32
scripts/items/item_affix_database.gd
Normal file
32
scripts/items/item_affix_database.gd
Normal file
@ -0,0 +1,32 @@
|
||||
extends Resource
|
||||
class_name ItemAffixDatabase
|
||||
|
||||
@export var affixes: Array[ItemAffixDefinition] = []
|
||||
|
||||
func roll_affixes(item: ItemDefinition, level: int, requested_count: int) -> Array[ItemStatModifier]:
|
||||
var pool: Array[ItemAffixDefinition] = []
|
||||
for affix in affixes:
|
||||
if affix != null and affix.can_roll(item, level):
|
||||
pool.append(affix)
|
||||
var modifiers: Array[ItemStatModifier] = []
|
||||
while modifiers.size() < requested_count and not pool.is_empty():
|
||||
var total_weight := 0.0
|
||||
for candidate in pool:
|
||||
total_weight += candidate.selection_weight
|
||||
var roll := randf() * total_weight
|
||||
var selected := pool.back() as ItemAffixDefinition
|
||||
for candidate in pool:
|
||||
roll -= candidate.selection_weight
|
||||
if roll <= 0.0:
|
||||
selected = candidate
|
||||
break
|
||||
var modifier := ItemStatModifier.new()
|
||||
modifier.affix = selected
|
||||
modifier.stat_key = selected.stat_key
|
||||
modifier.value = selected.roll_value(level)
|
||||
modifiers.append(modifier)
|
||||
# Removing incompatible candidates also prevents duplicate affix IDs.
|
||||
for index in range(pool.size() - 1, -1, -1):
|
||||
if not selected.can_combine(pool[index]):
|
||||
pool.remove_at(index)
|
||||
return modifiers
|
||||
1
scripts/items/item_affix_database.gd.uid
Normal file
1
scripts/items/item_affix_database.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://chgc2743hvpnn
|
||||
53
scripts/items/item_affix_definition.gd
Normal file
53
scripts/items/item_affix_definition.gd
Normal file
@ -0,0 +1,53 @@
|
||||
extends Resource
|
||||
class_name ItemAffixDefinition
|
||||
|
||||
@export var affix_id: String = ""
|
||||
@export var display_name: String = ""
|
||||
@export_multiline var description: String = ""
|
||||
@export var stat_key: String = ""
|
||||
@export_enum("%", "pkt", "/s", "px/s") var unit: String = "%"
|
||||
@export var enabled: bool = true
|
||||
@export_range(0.0, 1000.0, 0.1) var selection_weight: float = 1.0
|
||||
|
||||
@export_group("Dozwolone przedmioty")
|
||||
@export_flags("Weapon", "Shield", "Hull", "Generator", "Extension") var category_mask: int = 31
|
||||
# Empty means every item in the allowed categories.
|
||||
@export var allowed_item_ids: PackedStringArray = PackedStringArray()
|
||||
@export var excluded_item_ids: PackedStringArray = PackedStringArray()
|
||||
|
||||
@export_group("Zakres i skalowanie")
|
||||
@export_range(1, 1000) var minimum_level: int = 1
|
||||
@export_range(1, 1000) var maximum_level: int = 1000
|
||||
@export var minimum_value_level_1: float = 1.0
|
||||
@export var maximum_value_level_1: float = 3.0
|
||||
@export var minimum_value_level_1000: float = 25.0
|
||||
@export var maximum_value_level_1000: float = 50.0
|
||||
|
||||
@export_group("Zasady łączenia")
|
||||
# At most one affix from a nonempty group can appear on the same item.
|
||||
@export var exclusive_group: String = ""
|
||||
@export var incompatible_affix_ids: PackedStringArray = PackedStringArray()
|
||||
|
||||
func can_roll(item: ItemDefinition, level: int) -> bool:
|
||||
return (
|
||||
enabled and selection_weight > 0.0 and not affix_id.is_empty()
|
||||
and not stat_key.is_empty() and item != null
|
||||
and level >= minimum_level and level <= maximum_level
|
||||
and (category_mask & (1 << item.category)) != 0
|
||||
and (allowed_item_ids.is_empty() or item.item_id in allowed_item_ids)
|
||||
and item.item_id not in excluded_item_ids
|
||||
)
|
||||
|
||||
func can_combine(other: ItemAffixDefinition) -> bool:
|
||||
return (
|
||||
affix_id != other.affix_id
|
||||
and (exclusive_group.is_empty() or exclusive_group != other.exclusive_group)
|
||||
and other.affix_id not in incompatible_affix_ids
|
||||
and affix_id not in other.incompatible_affix_ids
|
||||
)
|
||||
|
||||
func roll_value(level: int) -> float:
|
||||
var progression := float(clampi(level, 1, 1000) - 1) / 999.0
|
||||
var lower := lerpf(minimum_value_level_1, minimum_value_level_1000, progression)
|
||||
var upper := lerpf(maximum_value_level_1, maximum_value_level_1000, progression)
|
||||
return randf_range(minf(lower, upper), maxf(lower, upper))
|
||||
1
scripts/items/item_affix_definition.gd.uid
Normal file
1
scripts/items/item_affix_definition.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cp6ucjqrac8so
|
||||
@ -19,3 +19,9 @@ enum WeaponKind { PLASMA, MACHINE_GUN, LASER, MISSILE }
|
||||
@export var projectile_speed: float = 0.0
|
||||
@export var weapon_range: float = 0.0
|
||||
@export var energy_cost: float = 0.0
|
||||
|
||||
@export_group("Losowanie zakresu obrażeń broni (poziom 1)")
|
||||
@export_range(1, 10000) var damage_lower_roll_min: int = 1
|
||||
@export_range(1, 10000) var damage_lower_roll_max: int = 4
|
||||
@export_range(1, 10000) var damage_spread_roll_min: int = 2
|
||||
@export_range(1, 10000) var damage_spread_roll_max: int = 4
|
||||
|
||||
41
scripts/items/item_generator.gd
Normal file
41
scripts/items/item_generator.gd
Normal file
@ -0,0 +1,41 @@
|
||||
extends RefCounted
|
||||
|
||||
const DEFINITIONS := [
|
||||
preload("res://resources/items/plasma_cannon.tres"),
|
||||
preload("res://resources/items/beam_laser.tres"),
|
||||
preload("res://resources/items/machine_gun.tres"),
|
||||
preload("res://resources/items/missile_launcher.tres"),
|
||||
preload("res://resources/items/reinforced_shield.tres"),
|
||||
preload("res://resources/items/hull_plating.tres"),
|
||||
preload("res://resources/items/compact_generator.tres"),
|
||||
preload("res://resources/items/engine_extension.tres"),
|
||||
preload("res://resources/items/tuned_thrusters.tres"),
|
||||
preload("res://resources/items/shield_recharger.tres"),
|
||||
]
|
||||
const AFFIX_DATABASE := preload("res://resources/items/affix_database.tres")
|
||||
|
||||
static func generate(level: int, enemy_tier: int = 0) -> ItemInstance:
|
||||
return generate_for_definition(DEFINITIONS.pick_random(), level, enemy_tier)
|
||||
|
||||
static func generate_for_definition(definition: ItemDefinition, level: int, enemy_tier: int = 0) -> ItemInstance:
|
||||
var item := ItemInstance.new()
|
||||
item.definition = definition
|
||||
item.item_level = clampi(level, 1, 1000)
|
||||
var scale_factor := 1.0 + 0.05 * (item.item_level - 1)
|
||||
item.primary_value_override = item.definition.primary_value * scale_factor * randf_range(0.8, 1.2)
|
||||
if definition.category == ItemDefinition.Category.WEAPON:
|
||||
var lower := randi_range(mini(definition.damage_lower_roll_min, definition.damage_lower_roll_max), maxi(definition.damage_lower_roll_min, definition.damage_lower_roll_max))
|
||||
var spread := randi_range(mini(definition.damage_spread_roll_min, definition.damage_spread_roll_max), maxi(definition.damage_spread_roll_min, definition.damage_spread_roll_max))
|
||||
item.damage_min = maxf(1.0, roundf(lower * scale_factor))
|
||||
item.damage_max = maxf(item.damage_min + 1.0, roundf((lower + spread) * scale_factor))
|
||||
item.primary_value_override = (item.damage_min + item.damage_max) * 0.5
|
||||
item.energy_regeneration_override = item.definition.energy_regeneration_bonus * scale_factor * randf_range(0.8, 1.2)
|
||||
var roll := randf()
|
||||
var count := 1 if roll < 0.5 else (2 if roll < 0.75 else (3 if roll < 0.89 else (4 if roll < 0.97 else 5)))
|
||||
if enemy_tier == 1:
|
||||
count = mini(5, count + 1)
|
||||
elif enemy_tier >= 2:
|
||||
count = maxi(4, count)
|
||||
item.additional_modifiers = AFFIX_DATABASE.roll_affixes(item.definition, item.item_level, count)
|
||||
item.rarity = clampi(item.additional_modifiers.size() - 1, 0, 4)
|
||||
return item
|
||||
1
scripts/items/item_generator.gd.uid
Normal file
1
scripts/items/item_generator.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://3mi6rla5f5r8
|
||||
@ -4,14 +4,85 @@ class_name ItemInstance
|
||||
@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 += definition.primary_value
|
||||
total += primary_value_override if primary_value_override >= 0.0 else definition.primary_value
|
||||
if definition and stat_key == "energy_regeneration":
|
||||
total += definition.energy_regeneration_bonus
|
||||
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
|
||||
|
||||
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_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 [Color("b9c5d4"), Color("63df91"), Color("65b4ff"), Color("bd83ff"), Color("ffca63")][clampi(rarity, 0, 4)]
|
||||
|
||||
func get_tooltip_text() -> String:
|
||||
if definition == null:
|
||||
return ""
|
||||
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()
|
||||
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))
|
||||
if definition.category == ItemDefinition.Category.WEAPON:
|
||||
lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.")
|
||||
lines.append("Zasięg: %.0f px • Energia: %.1f / strzał" % [definition.weapon_range, definition.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({"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"}.get(key, key))
|
||||
|
||||
@ -3,3 +3,9 @@ class_name ItemStatModifier
|
||||
|
||||
@export var stat_key: String = ""
|
||||
@export var value: float = 0.0
|
||||
@export var affix: ItemAffixDefinition
|
||||
|
||||
func get_display_text() -> String:
|
||||
var label := affix.display_name if affix else ItemInstance.stat_name(stat_key)
|
||||
var suffix := affix.unit if affix else ("%" if stat_key.ends_with("_percent") else "/s")
|
||||
return "%s: %s%.2f %s" % [label, "+" if value >= 0.0 else "", value, suffix]
|
||||
|
||||
46
scripts/items/loot_pickup.gd
Normal file
46
scripts/items/loot_pickup.gd
Normal file
@ -0,0 +1,46 @@
|
||||
extends Node2D
|
||||
|
||||
const PICKUP_RADIUS := 48.0
|
||||
const ATTRACTION_RADIUS := 150.0
|
||||
var item: ItemInstance
|
||||
var _player: CharacterBody2D
|
||||
var _label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("loot")
|
||||
_player = get_parent().get_node_or_null("PlayerShip") as CharacterBody2D
|
||||
_label = Label.new()
|
||||
_label.position = Vector2(-110, 18)
|
||||
_label.size = Vector2(220, 44)
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.add_theme_font_size_override("font_size", 11)
|
||||
_label.add_theme_color_override("font_color", item.get_rarity_color())
|
||||
_label.text = item.definition.display_name
|
||||
add_child(_label)
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
if item == null:
|
||||
return
|
||||
var color := item.get_rarity_color()
|
||||
draw_circle(Vector2.ZERO, 15.0, Color(color, 0.16))
|
||||
draw_arc(Vector2.ZERO, 15.0, 0.0, TAU, 32, color, 2.0, true)
|
||||
draw_colored_polygon(PackedVector2Array([Vector2(0, -9), Vector2(8, 0), Vector2(0, 9), Vector2(-8, 0)]), color)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):
|
||||
return
|
||||
var distance := global_position.distance_to(_player.global_position)
|
||||
if distance > ATTRACTION_RADIUS:
|
||||
return
|
||||
var inventory := _player.get_node("ShipInventory") as ShipInventory
|
||||
if inventory.get_cargo_used() >= ShipInventory.CARGO_SIZE:
|
||||
_label.text = "%s\nMagazyn pełny" % item.definition.display_name
|
||||
return
|
||||
_label.text = item.definition.display_name
|
||||
if distance <= PICKUP_RADIUS:
|
||||
if inventory.add_item(item):
|
||||
set_physics_process(false)
|
||||
queue_free()
|
||||
else:
|
||||
global_position = global_position.move_toward(_player.global_position, 180.0 * delta)
|
||||
1
scripts/items/loot_pickup.gd.uid
Normal file
1
scripts/items/loot_pickup.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://ndu1j4ns1lj7
|
||||
@ -49,6 +49,23 @@ func get_item(slot_kind: String, index: int) -> ItemInstance:
|
||||
return null
|
||||
return slots[index]
|
||||
|
||||
func add_item(item: ItemInstance) -> bool:
|
||||
if item == null or item.definition == null:
|
||||
return false
|
||||
for index in cargo.size():
|
||||
if cargo[index] == null:
|
||||
cargo[index] = item
|
||||
inventory_changed.emit()
|
||||
return true
|
||||
return false
|
||||
|
||||
func delete_cargo_item(index: int) -> bool:
|
||||
if index < 0 or index >= cargo.size() or cargo[index] == null:
|
||||
return false
|
||||
cargo[index] = null
|
||||
inventory_changed.emit()
|
||||
return true
|
||||
|
||||
func can_move_item(from_kind: String, from_index: int, to_kind: String, to_index: int) -> bool:
|
||||
var source := _get_slots(from_kind)
|
||||
var destination := _get_slots(to_kind)
|
||||
@ -98,7 +115,7 @@ func _recalculate_ship_stats() -> void:
|
||||
for item in weapon_slots + module_slots:
|
||||
if item == null or item.definition == null:
|
||||
continue
|
||||
for stat_key in ["max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration"]:
|
||||
for stat_key in ["max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent"]:
|
||||
bonuses[stat_key] = float(bonuses.get(stat_key, 0.0)) + item.get_stat_total(stat_key)
|
||||
ship.call("apply_equipment_bonuses", bonuses)
|
||||
|
||||
|
||||
@ -6,8 +6,26 @@ extends PanelContainer
|
||||
@onready var slot_label: Label = $SlotLabel
|
||||
|
||||
var _inventory: ShipInventory
|
||||
var _delete_button: Button
|
||||
|
||||
func _ready() -> void:
|
||||
if slot_kind == "cargo":
|
||||
var overlay := Control.new()
|
||||
overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(overlay)
|
||||
_delete_button = Button.new()
|
||||
_delete_button.text = "×"
|
||||
_delete_button.tooltip_text = "Usuń przedmiot z magazynu"
|
||||
_delete_button.flat = true
|
||||
_delete_button.add_theme_font_size_override("font_size", 13)
|
||||
_delete_button.add_theme_color_override("font_color", Color("ff8989"))
|
||||
overlay.add_child(_delete_button)
|
||||
_delete_button.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
|
||||
_delete_button.offset_left = -20
|
||||
_delete_button.offset_right = 0
|
||||
_delete_button.offset_top = 0
|
||||
_delete_button.offset_bottom = 18
|
||||
_delete_button.pressed.connect(_delete_item)
|
||||
_inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory
|
||||
if _inventory:
|
||||
_inventory.inventory_changed.connect(_refresh)
|
||||
@ -24,10 +42,15 @@ func _refresh() -> void:
|
||||
if not is_node_ready() or not _inventory:
|
||||
return
|
||||
var item: ItemInstance = _inventory.get_item(slot_kind, slot_index)
|
||||
tooltip_text = item.get_tooltip_text() if item != null else ""
|
||||
if _delete_button:
|
||||
_delete_button.visible = item != null
|
||||
if item == null or item.definition == null:
|
||||
slot_label.add_theme_color_override("font_color", Color("82909e"))
|
||||
slot_label.text = _empty_slot_text()
|
||||
return
|
||||
var definition := item.definition
|
||||
slot_label.add_theme_color_override("font_color", item.get_rarity_color())
|
||||
var category_name := _category_name(definition.category)
|
||||
var value_text := _stat_text(item)
|
||||
var slot_title := category_name
|
||||
@ -38,6 +61,29 @@ func _refresh() -> void:
|
||||
slot_title = "MODUŁ %d [%d]" % [slot_index + 1, slot_index + 1]
|
||||
slot_label.text = "%s\n%s\n%s" % [slot_title, definition.display_name, value_text]
|
||||
|
||||
func _delete_item() -> void:
|
||||
if _inventory:
|
||||
_inventory.delete_cargo_item(slot_index)
|
||||
|
||||
func _make_custom_tooltip(for_text: String) -> Object:
|
||||
var panel := PanelContainer.new()
|
||||
var background := StyleBoxFlat.new()
|
||||
background.bg_color = Color("101925")
|
||||
background.border_color = Color("52677e")
|
||||
background.set_border_width_all(1)
|
||||
background.set_content_margin_all(12)
|
||||
panel.add_theme_stylebox_override("panel", background)
|
||||
var label := Label.new()
|
||||
label.text = for_text
|
||||
label.custom_minimum_size.x = 330
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
var item := _inventory.get_item(slot_kind, slot_index)
|
||||
if item:
|
||||
label.add_theme_color_override("font_color", item.get_rarity_color())
|
||||
panel.add_child(label)
|
||||
return panel
|
||||
|
||||
func _get_drag_data(_at_position: Vector2) -> Variant:
|
||||
if not _inventory:
|
||||
return null
|
||||
@ -94,6 +140,8 @@ func _category_name(category: int) -> String:
|
||||
|
||||
func _stat_text(item: ItemInstance) -> String:
|
||||
var definition := item.definition
|
||||
if definition.category == ItemDefinition.Category.WEAPON:
|
||||
return "%s · Poz. %d" % [item.get_damage_range_text(), item.item_level]
|
||||
if definition.primary_stat.is_empty():
|
||||
return "Poz. %d" % item.item_level
|
||||
var total_value := item.get_stat_total(definition.primary_stat)
|
||||
|
||||
71
tests/items/affix_database_test.gd
Normal file
71
tests/items/affix_database_test.gd
Normal file
@ -0,0 +1,71 @@
|
||||
extends Node
|
||||
|
||||
const GENERATOR := preload("res://scripts/items/item_generator.gd")
|
||||
const DATABASE := preload("res://resources/items/affix_database.tres")
|
||||
|
||||
func _ready() -> void:
|
||||
assert(DATABASE.affixes.size() == 8)
|
||||
for level in [1, 50, 1000]:
|
||||
for tier in [0, 1, 2]:
|
||||
for sample in 50:
|
||||
var item := GENERATOR.generate(level, tier)
|
||||
assert(item.item_level == level)
|
||||
assert(item.additional_modifiers.size() >= 1 and item.additional_modifiers.size() <= 5)
|
||||
if tier == 2:
|
||||
assert(item.additional_modifiers.size() >= 4)
|
||||
var ids: Array[String] = []
|
||||
for modifier in item.additional_modifiers:
|
||||
assert(modifier.affix.can_roll(item.definition, level))
|
||||
assert(modifier.affix.affix_id not in ids)
|
||||
ids.append(modifier.affix.affix_id)
|
||||
var progression := float(level - 1) / 999.0
|
||||
assert(modifier.value >= lerpf(modifier.affix.minimum_value_level_1, modifier.affix.minimum_value_level_1000, progression))
|
||||
assert(modifier.value <= lerpf(modifier.affix.maximum_value_level_1, modifier.affix.maximum_value_level_1000, progression))
|
||||
assert(item.get_tooltip_text().contains(item.additional_modifiers[0].affix.display_name))
|
||||
|
||||
var weapon := ItemDefinition.new()
|
||||
weapon.item_id = "test_weapon"
|
||||
weapon.category = ItemDefinition.Category.WEAPON
|
||||
var shield := ItemDefinition.new()
|
||||
shield.item_id = "test_shield"
|
||||
shield.category = ItemDefinition.Category.SHIELD
|
||||
var first := ItemAffixDefinition.new()
|
||||
first.affix_id = "first"
|
||||
first.stat_key = "damage_percent"
|
||||
first.category_mask = 1
|
||||
assert(first.can_roll(weapon, 1))
|
||||
assert(not first.can_roll(shield, 1))
|
||||
first.allowed_item_ids = PackedStringArray(["another_weapon"])
|
||||
assert(not first.can_roll(weapon, 1))
|
||||
first.allowed_item_ids = PackedStringArray([weapon.item_id])
|
||||
assert(first.can_roll(weapon, 1))
|
||||
first.excluded_item_ids = PackedStringArray([weapon.item_id])
|
||||
assert(not first.can_roll(weapon, 1))
|
||||
first.excluded_item_ids.clear()
|
||||
first.minimum_level = 10
|
||||
first.maximum_level = 20
|
||||
assert(not first.can_roll(weapon, 1))
|
||||
assert(first.can_roll(weapon, 15))
|
||||
assert(not first.can_roll(weapon, 21))
|
||||
first.minimum_level = 1
|
||||
first.maximum_level = 1000
|
||||
|
||||
var second := first.duplicate() as ItemAffixDefinition
|
||||
second.affix_id = "second"
|
||||
var database := ItemAffixDatabase.new()
|
||||
database.affixes.assign([first, second])
|
||||
assert(database.roll_affixes(weapon, 1, 5).size() == 2)
|
||||
first.exclusive_group = "test_group"
|
||||
second.exclusive_group = "test_group"
|
||||
assert(database.roll_affixes(weapon, 1, 5).size() == 1)
|
||||
first.exclusive_group = ""
|
||||
second.exclusive_group = ""
|
||||
first.incompatible_affix_ids = PackedStringArray([second.affix_id])
|
||||
assert(not first.can_combine(second) and not second.can_combine(first))
|
||||
assert(database.roll_affixes(weapon, 1, 5).size() == 1)
|
||||
first.incompatible_affix_ids.clear()
|
||||
first.selection_weight = 0.0
|
||||
assert(database.roll_affixes(weapon, 1, 5)[0].affix == second)
|
||||
second.enabled = false
|
||||
assert(database.roll_affixes(weapon, 1, 5).is_empty())
|
||||
print("AFFIX_DATABASE_TEST: PASS (450 generated items, category/ID/level filters, exclusions, weights and empty pools)")
|
||||
1
tests/items/affix_database_test.gd.uid
Normal file
1
tests/items/affix_database_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bhpv7xasq34qg
|
||||
6
tests/items/affix_database_test.tscn
Normal file
6
tests/items/affix_database_test.tscn
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/items/affix_database_test.gd" id="1_test"]
|
||||
|
||||
[node name="AffixDatabaseTest" type="Node"]
|
||||
script = ExtResource("1_test")
|
||||
108
tests/items/weapon_damage_test.gd
Normal file
108
tests/items/weapon_damage_test.gd
Normal file
@ -0,0 +1,108 @@
|
||||
extends Node2D
|
||||
|
||||
const GENERATOR := preload("res://scripts/items/item_generator.gd")
|
||||
const PLAYER_SCENE := preload("res://scenes/game/player_ship.tscn")
|
||||
const PROJECTILE_SCRIPT := preload("res://scripts/game/weapon_projectile.gd")
|
||||
|
||||
class LaserProbe:
|
||||
extends "res://scripts/game/weapon_controller.gd"
|
||||
var hits: Array[float] = []
|
||||
func _aim_direction_from(_origin: Vector2) -> Vector2:
|
||||
return Vector2.RIGHT
|
||||
func _apply_damage(_target: Variant, damage: float) -> void:
|
||||
hits.append(damage)
|
||||
|
||||
func _ready() -> void:
|
||||
var saw_laser_1_5 := false
|
||||
var saw_laser_4_6 := false
|
||||
for definition in GENERATOR.DEFINITIONS:
|
||||
if definition.category != ItemDefinition.Category.WEAPON:
|
||||
continue
|
||||
for level in [1, 50, 1000]:
|
||||
for sample in 100:
|
||||
var item := GENERATOR.generate_for_definition(definition, level)
|
||||
var bounds := item.get_damage_range()
|
||||
assert(bounds.x >= 1 and bounds.y > bounds.x)
|
||||
var scale_factor: float = 1.0 + 0.05 * (level - 1)
|
||||
assert(bounds.x >= roundf(definition.damage_lower_roll_min * scale_factor))
|
||||
assert(bounds.x <= roundf(definition.damage_lower_roll_max * scale_factor))
|
||||
assert(bounds.y <= roundf((definition.damage_lower_roll_max + definition.damage_spread_roll_max) * scale_factor))
|
||||
for shot in 20:
|
||||
var damage := item.roll_damage()
|
||||
assert(damage >= bounds.x and damage <= bounds.y)
|
||||
assert(item.get_damage_range() == bounds)
|
||||
assert(item.get_tooltip_text().contains(item.get_damage_range_text()))
|
||||
if definition.item_id == "beam_laser" and level == 1:
|
||||
saw_laser_1_5 = saw_laser_1_5 or bounds == Vector2(1, 5)
|
||||
saw_laser_4_6 = saw_laser_4_6 or bounds == Vector2(4, 6)
|
||||
assert(saw_laser_1_5 and saw_laser_4_6)
|
||||
|
||||
var laser := ItemInstance.new()
|
||||
laser.definition = preload("res://resources/items/beam_laser.tres")
|
||||
laser.damage_min = 1
|
||||
laser.damage_max = 5
|
||||
var results: Dictionary = {}
|
||||
for shot in 300:
|
||||
results[laser.roll_damage()] = true
|
||||
assert(results.size() == 5 and results.has(1.0) and results.has(5.0))
|
||||
var legacy := ItemInstance.new()
|
||||
legacy.definition = laser.definition
|
||||
legacy.primary_value_override = 7.5
|
||||
assert(legacy.roll_damage() == 7.5)
|
||||
|
||||
var player := PLAYER_SCENE.instantiate() as CharacterBody2D
|
||||
add_child(player)
|
||||
player.set_physics_process(false)
|
||||
player.get_node("WeaponController").set_physics_process(false)
|
||||
player.set("damage_multiplier", 1.25)
|
||||
var probe := LaserProbe.new()
|
||||
player.add_child(probe)
|
||||
probe.set_physics_process(false)
|
||||
var target := StaticBody2D.new()
|
||||
target.position = Vector2(200, -28)
|
||||
target.collision_layer = 4
|
||||
var collision := CollisionShape2D.new()
|
||||
var shape := CircleShape2D.new()
|
||||
shape.radius = 20
|
||||
collision.shape = shape
|
||||
target.add_child(collision)
|
||||
add_child(target)
|
||||
await get_tree().physics_frame
|
||||
await get_tree().physics_frame
|
||||
for shot in 100:
|
||||
player.set("energy", 100.0)
|
||||
probe.set("_shot_timers", [0.0, 0.0])
|
||||
probe._fire_laser(0, laser)
|
||||
assert(probe.hits.size() == 100)
|
||||
var laser_hits: Dictionary = {}
|
||||
for hit in probe.hits:
|
||||
assert(hit >= 1.25 and hit <= 6.25)
|
||||
laser_hits[hit] = true
|
||||
assert(laser_hits.size() == 5)
|
||||
player.set("energy", 0.0)
|
||||
probe.set("_shot_timers", [0.0, 0.0])
|
||||
probe._fire_laser(0, laser)
|
||||
assert(probe.hits.size() == 100)
|
||||
player.set("energy", 100.0)
|
||||
probe.set("_shot_timers", [10.0, 0.0])
|
||||
probe._fire_laser(0, laser)
|
||||
assert(probe.hits.size() == 100)
|
||||
|
||||
for definition in GENERATOR.DEFINITIONS:
|
||||
if definition.category != ItemDefinition.Category.WEAPON or definition.weapon_kind == ItemDefinition.WeaponKind.LASER:
|
||||
continue
|
||||
var item := GENERATOR.generate_for_definition(definition, 1)
|
||||
var bounds := item.get_damage_range()
|
||||
var projectile_results: Dictionary = {}
|
||||
for shot in 100:
|
||||
var previous_child_count := get_child_count()
|
||||
probe._fire_projectile(item)
|
||||
var projectile := get_child(previous_child_count)
|
||||
assert(projectile.get_script() == PROJECTILE_SCRIPT)
|
||||
var damage := float(projectile.get("damage"))
|
||||
assert(damage >= bounds.x * 1.25 and damage <= bounds.y * 1.25)
|
||||
projectile_results[damage] = true
|
||||
remove_child(projectile)
|
||||
projectile.free()
|
||||
assert(projectile_results.size() > 1)
|
||||
print("WEAPON_DAMAGE_TEST: PASS (1200 weapon rolls, 24000 damage rolls, 100 laser pulses and 300 projectiles; bonuses and energy/cooldown gates)")
|
||||
1
tests/items/weapon_damage_test.gd.uid
Normal file
1
tests/items/weapon_damage_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cfpa84wi27vr6
|
||||
6
tests/items/weapon_damage_test.tscn
Normal file
6
tests/items/weapon_damage_test.tscn
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/items/weapon_damage_test.gd" id="1_test"]
|
||||
|
||||
[node name="WeaponDamageTest" type="Node2D"]
|
||||
script = ExtResource("1_test")
|
||||
Loading…
x
Reference in New Issue
Block a user