extends Node class_name ShipInventory signal inventory_changed signal ammunition_changed const CARGO_SIZE := 20 const WEAPON_SLOT_COUNT := 3 const STARTING_MISSILE_AMMUNITION := 2 const MISSILE_CRAFT_PARTS_COST := 50.0 const MISSILE_BUY_BASE_COST := 10 const MISSILE_DEFINITIONS := [preload("res://resources/items/missile_launcher.tres"), preload("res://resources/items/unguided_missile.tres")] const MODULE_SLOT_COUNT := 5 const EXTENSION_SLOT_COUNT := 5 const MODULE_CATEGORIES := [ItemDefinition.Category.SHIELD, ItemDefinition.Category.ARMOR, ItemDefinition.Category.ENGINE, ItemDefinition.Category.REACTOR, ItemDefinition.Category.COMPUTER] const MODULE_NAMES := ["Tarcze", "Pancerz", "Silnik", "Reaktor", "Komputer"] const STARTING_AMMUNITION := 50 const AMMUNITION_CRAFT_AMOUNT := 100 const AMMUNITION_CRAFT_PARTS_COST := 50.0 const AMMUNITION_BUY_AMOUNT := 100 const AMMUNITION_BUY_BASE_COST := 20 var ammunition: int = STARTING_AMMUNITION var missile_ammunition: int = STARTING_MISSILE_AMMUNITION var cargo: Array[ItemInstance] = [] var weapon_slots: Array[ItemInstance] = [] var module_slots: Array[ItemInstance] = [] var extension_slots: Array[ItemInstance] = [] @onready var ship: CharacterBody2D = get_parent() func _ready() -> void: add_to_group("ship_inventory") cargo.resize(CARGO_SIZE) weapon_slots.resize(WEAPON_SLOT_COUNT) module_slots.resize(MODULE_SLOT_COUNT) extension_slots.resize(EXTENSION_SLOT_COUNT) if GameState.restart_inventory.is_empty(): _add_starter_items() else: cargo.assign(GameState.restart_inventory["cargo"]) weapon_slots.assign(GameState.restart_inventory["weapon"]) weapon_slots.resize(WEAPON_SLOT_COUNT) _restore_modules(GameState.restart_inventory) ammunition = int(GameState.restart_inventory.get("ammunition", STARTING_AMMUNITION)) missile_ammunition = int(GameState.restart_inventory.get("missile_ammunition", STARTING_MISSILE_AMMUNITION)) GameState.restart_inventory.clear() _remove_extension_addons() _recalculate_ship_stats() inventory_changed.emit() func get_cargo_used() -> int: var used := 0 for item in cargo: if item != null: used += 1 return used func preserve_for_restart() -> void: # Copy the slot arrays; resources retain levels and individual modifiers. GameState.restart_inventory = { "cargo": cargo.duplicate(), "weapon": weapon_slots.duplicate(), "module": module_slots.duplicate(), "extension": extension_slots.duplicate(), "ammunition": ammunition, "missile_ammunition": missile_ammunition, } func consume_ammunition() -> bool: if ammunition <= 0: return false ammunition -= 1 ammunition_changed.emit() return true func can_craft_ammunition() -> bool: return GameState.captain.parts >= AMMUNITION_CRAFT_PARTS_COST func craft_ammunition() -> bool: if not can_craft_ammunition(): return false GameState.captain.parts -= AMMUNITION_CRAFT_PARTS_COST ammunition += AMMUNITION_CRAFT_AMOUNT GameState.captain.emit_changed() ammunition_changed.emit() return true func get_item(slot_kind: String, index: int) -> ItemInstance: var slots := _get_slots(slot_kind) if index < 0 or index >= slots.size(): return null return slots[index] func get_ammunition_buy_cost() -> int: return AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level) func can_buy_ammunition() -> bool: return GameState.captain.credits >= get_ammunition_buy_cost() func buy_ammunition() -> bool: if not can_buy_ammunition(): return false GameState.captain.credits -= get_ammunition_buy_cost() ammunition += AMMUNITION_BUY_AMOUNT GameState.captain.emit_changed() ammunition_changed.emit() return true func add_item(item: ItemInstance) -> bool: if item == null or item.definition == null: return false if item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION: item.additional_modifiers.clear() item.rarity = 0 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 GameState.captain.parts += 10.0 GameState.captain.emit_changed() 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) if from_index < 0 or from_index >= source.size() or to_index < 0 or to_index >= destination.size(): return false if from_kind == to_kind and from_index == to_index: return false var item := source[from_index] if item == null or item.definition == null: return false if not _accepts_item(to_kind, to_index, item): return false var displaced := destination[to_index] if displaced != null and displaced.definition != null: if not _accepts_item(from_kind, from_index, displaced): return false return true func _accepts_item(kind: String, index: int, item: ItemInstance) -> bool: if kind == "weapon": return item.definition.category == ItemDefinition.Category.WEAPON if kind == "module": return item.definition.category == MODULE_CATEGORIES[index] if kind == "extension": return item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and item.definition.active_ability != null return kind == "cargo" func get_missile_count() -> int: return missile_ammunition func get_missile_cooldown(item: ItemInstance) -> float: var reduction := item.get_stat_total("missile_cooldown_reduction") for equipped in weapon_slots + module_slots: if equipped != null and equipped != item: reduction += equipped.get_stat_total("missile_cooldown_reduction") return maxf(1.0, 5.0 - reduction) func consume_missile() -> bool: if missile_ammunition <= 0: return false missile_ammunition -= 1 ammunition_changed.emit() return true func can_craft_missile() -> bool: return GameState.captain.parts >= MISSILE_CRAFT_PARTS_COST func get_missile_buy_cost() -> int: return MISSILE_BUY_BASE_COST * maxi(1, GameState.system_level) func can_buy_missile() -> bool: return GameState.captain.credits >= get_missile_buy_cost() func craft_missile() -> bool: if not can_craft_missile(): return false GameState.captain.parts -= MISSILE_CRAFT_PARTS_COST missile_ammunition += 1 GameState.captain.emit_changed() ammunition_changed.emit() return true func buy_missile() -> bool: if not can_buy_missile(): return false GameState.captain.credits -= get_missile_buy_cost() missile_ammunition += 1 GameState.captain.emit_changed() ammunition_changed.emit() return true func move_item(from_kind: String, from_index: int, to_kind: String, to_index: int) -> bool: if not can_move_item(from_kind, from_index, to_kind, to_index): return false var source := _get_slots(from_kind) var destination := _get_slots(to_kind) var displaced := destination[to_index] destination[to_index] = source[from_index] source[from_index] = displaced _remove_extension_addons() _recalculate_ship_stats() inventory_changed.emit() return true func _get_slots(slot_kind: String) -> Array[ItemInstance]: match slot_kind: "cargo": return cargo "weapon": return weapon_slots "module": return module_slots "extension": return extension_slots return [] func _recalculate_ship_stats() -> void: var bonuses := {} for item in weapon_slots + module_slots: if item == null or item.definition == null: continue for stat_key in ["shield_on_damage_percent", "scanner_range", "max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "critical_chance_percent", "critical_damage_percent"]: bonuses[stat_key] = float(bonuses.get(stat_key, 0.0)) + item.get_stat_total(stat_key) ship.call("apply_equipment_bonuses", bonuses) func _add_starter_items() -> void: var definition := load("res://resources/items/plasma_cannon.tres") as ItemDefinition if definition == null: push_warning("Could not load starter plasma cannon") return var item := ItemInstance.new() item.definition = definition item.item_level = 1 weapon_slots[0] = item func _remove_extension_addons() -> void: for item in cargo + extension_slots: if item != null and item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION: item.additional_modifiers.clear() item.rarity = 0 func _restore_modules(saved: Dictionary) -> void: if saved.has("extension"): extension_slots.assign(saved["extension"]) extension_slots.resize(EXTENSION_SLOT_COUNT) for item in saved.get("module", []): if item == null or item.definition == null: continue var index := MODULE_CATEGORIES.find(item.definition.category) if index >= 0 and module_slots[index] == null: module_slots[index] = item elif item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and extension_slots.has(null): extension_slots[extension_slots.find(null)] = item else: add_item(item) func get_item_comparison(item: ItemInstance, equipped: ItemInstance) -> Array[Dictionary]: var before := _comparison_stats(_comparison_bonuses()) var after := _comparison_stats(_comparison_bonuses(item, equipped)) var rows: Array[Dictionary] = [] for key in before: var difference: float = float(after[key]["value"]) - float(before[key]["value"]) if is_zero_approx(difference): continue rows.append({"key": key, "name": before[key]["name"], "delta": difference, "unit": before[key]["unit"]}) return rows func _comparison_bonuses(incoming: ItemInstance = null, outgoing: ItemInstance = null) -> Dictionary: var bonuses := {} for item in weapon_slots + module_slots: if item == null or item == outgoing: continue _add_comparison_bonuses(bonuses, item) if incoming != null: _add_comparison_bonuses(bonuses, incoming) return bonuses func _add_comparison_bonuses(bonuses: Dictionary, item: ItemInstance) -> void: for key in ["max_hull", "max_shields", "max_energy", "max_speed", "scanner_range", "hull_percent", "shield_percent", "speed_percent", "damage_percent", "critical_damage_percent", "critical_chance_percent", "shield_on_damage_percent", "energy_regeneration", "shield_regeneration", "missile_cooldown_reduction", "experience_percent", "credits_percent"]: bonuses[key] = float(bonuses.get(key, 0.0)) + item.get_stat_total(key) func _comparison_stats(bonuses: Dictionary) -> Dictionary: var stats := {} for data in [["max_hull", "base_max_hull", "hull_percent", "Pancerz", "pkt"], ["max_shields", "base_max_shields", "shield_percent", "Tarcze", "pkt"], ["max_speed", "base_max_speed", "speed_percent", "Prędkość", "px/s"]]: var value := (float(ship.get(data[1])) + float(bonuses.get(data[0], 0.0))) * (1.0 + float(bonuses.get(data[2], 0.0)) / 100.0) stats[data[0]] = {"value": value, "name": data[3], "unit": data[4]} for data in [["max_energy", "base_max_energy", "Energia", "pkt"], ["scanner_range", "base_scanner_range", "Zasięg skanera", "px"], ["energy_regeneration", "base_energy_regeneration", "Regeneracja energii", "pkt/s"], ["shield_regeneration", "base_shield_regeneration", "Regeneracja tarcz", "pkt/s"]]: stats[data[0]] = {"value": float(ship.get(data[1])) + float(bonuses.get(data[0], 0.0)), "name": data[2], "unit": data[3]} var captain_damage := 1.0 + GameState.captain.get_skill_bonus("gunner") var damage_factor := (1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0) * captain_damage var critical_factor := preload("res://scripts/captain/captain_balance.gd").BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, float(bonuses.get("critical_damage_percent", 0.0)) / 100.0)) for index in weapon_slots.size(): var weapon := weapon_slots[index] if weapon == null: continue var damage := weapon.get_stat_total("damage") * damage_factor var binding: String = ["LPM", "PPM", "ŚPM"][index] stats["weapon_damage_%d" % index] = {"value": damage, "name": "Śr. obrażenia [%s]" % binding, "unit": "DMG"} stats["weapon_critical_%d" % index] = {"value": damage * critical_factor, "name": "Śr. krytyk [%s]" % binding, "unit": "DMG"} if weapon.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: stats["missile_cooldown_%d" % index] = {"value": -maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))), "name": "Skrócenie cooldownu [%s]" % binding, "unit": "s"} var chance := clampf(GameState.captain.get_skill_bonus("sharp_eye") + float(bonuses.get("critical_chance_percent", 0.0)) / 100.0, 0.0, 1.0) stats["critical_chance"] = {"value": chance * 100.0, "name": "Krytyki na 100 strzałów", "unit": "trafień"} stats["shield_recovery"] = {"value": maxf(0.0, float(bonuses.get("shield_on_damage_percent", 0.0))), "name": "Odzysk tarcz na 100 DMG", "unit": "pkt"} var balance := preload("res://scripts/captain/captain_balance.gd") var scale_factor := 1.0 + 0.05 * (clampi(GameState.system_level, 1, 1000) - 1) for tier in 3: var caption: String = ["wróg", "elita", "boss"][tier] stats["xp_%d" % tier] = {"value": roundi(balance.KILL_XP[tier] * scale_factor * maxf(0.0, 1.0 + float(bonuses.get("experience_percent", 0.0)) / 100.0)), "name": "EXP / %s" % caption, "unit": "EXP"} stats["credits_%d" % tier] = {"value": roundi(balance.KILL_CREDITS[tier] * scale_factor * maxf(0.0, 1.0 + float(bonuses.get("credits_percent", 0.0)) / 100.0)), "name": "Kredyty / %s" % caption, "unit": "kr"} return stats