Randoloot/scripts/items/ship_inventory.gd
2026-10-17 16:51:07 +02:00

452 lines
18 KiB
GDScript

extends Node
class_name ShipInventory
signal inventory_changed
signal ammunition_changed
const RECIPES := preload("res://scripts/items/crafting_recipes.gd")
const CARGO_SIZE := 20
const WEAPON_SLOT_COUNT := 3
const STARTING_MISSILE_AMMUNITION := 2
const MISSILE_BUY_BASE_COST := 30
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 := 10
const AMMUNITION_BUY_AMOUNT := 100
const AMMUNITION_BUY_BASE_COST := 60
const IRRADIATED_AMMUNITION_BUY_BASE_COST := 50
var extension_cooldowns: Dictionary = {}
var energy_banks := 0
var irradiated_ammunition := 0
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))
irradiated_ammunition = int(GameState.restart_inventory.get("irradiated_ammunition", 0))
energy_banks = int(GameState.restart_inventory.get("energy_banks", 0))
GameState.restart_inventory.clear()
_convert_legacy_energy_banks()
_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(),
"irradiated_ammunition": irradiated_ammunition,
"energy_banks": energy_banks,
"ammunition": ammunition,
"missile_ammunition": missile_ammunition,
}
func _convert_legacy_energy_banks() -> void:
for index in cargo.size():
if cargo[index] != null and cargo[index].definition != null and cargo[index].definition.item_id == "energy_bank":
energy_banks += 1
cargo[index] = null
func get_energy_bank_count() -> int:
return energy_banks
func use_energy_bank() -> bool:
if bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")) or get_tree().paused or energy_banks <= 0:
return false
var maximum := float(ship.get("max_energy"))
var current := float(ship.get("energy"))
if current >= maximum:
return false
ship.set("energy", minf(maximum, current + maximum * GameState.captain.get_energy_bank_fraction()))
energy_banks -= 1
inventory_changed.emit()
return true
func consume_ammunition() -> bool:
if ammunition <= 0:
return false
if GameState.captain.should_consume_ammunition("ordinary"):
ammunition -= 1
ammunition_changed.emit()
return true
func can_craft(recipe_id: String) -> bool:
if not RECIPES.DATA.has(recipe_id):
return false
for mineral in RECIPES.DATA[recipe_id]["cost"]:
if int(GameState.captain.minerals.get(mineral, 0)) < int(RECIPES.DATA[recipe_id]["cost"][mineral]):
return false
return true
func craft(recipe_id: String) -> bool:
if not can_craft(recipe_id):
return false
var recipe: Dictionary = RECIPES.DATA[recipe_id]
for mineral in recipe["cost"]:
GameState.captain.minerals[mineral] = int(GameState.captain.minerals.get(mineral, 0)) - int(recipe["cost"][mineral])
var amount := int(recipe["amount"])
match recipe_id:
"parts": GameState.captain.parts += amount
"ammunition": ammunition += amount
"missile": missile_ammunition += amount
"energy_bank": energy_banks += amount
"irradiated_ammunition": irradiated_ammunition += amount
GameState.captain.emit_changed()
ammunition_changed.emit()
inventory_changed.emit()
return true
func get_crafted_stock(recipe_id: String) -> int:
match recipe_id:
"parts": return int(GameState.captain.parts)
"ammunition": return ammunition
"missile": return missile_ammunition
"energy_bank": return energy_banks
"irradiated_ammunition": return irradiated_ammunition
return 0
func can_craft_ammunition() -> bool:
return can_craft("ammunition")
func craft_ammunition() -> bool:
return craft("ammunition")
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 GameState.captain.discounted_price(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.item_id == "energy_bank":
energy_banks += 1
inventory_changed.emit()
return true
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
if from_kind == "extension" or to_kind == "extension":
var projected := extension_slots.duplicate()
if from_kind == "extension":
projected[from_index] = displaced
if to_kind == "extension":
projected[to_index] = item
var equipped_types := {}
for extension in projected:
if extension == null or extension.definition == null:
continue
var type_id: String = extension.definition.item_id
if equipped_types.has(type_id):
return false
equipped_types[type_id] = true
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 or item.is_passive_extension())
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 can_craft("missile")
func get_missile_buy_cost() -> int:
return GameState.captain.discounted_price(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:
return craft("missile")
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 ["kinetic_absorption", "energy_absorption", "radiation_absorption", "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", "luck_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 ["kinetic_absorption", "energy_absorption", "radiation_absorption", "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", "luck_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]}
# These stats are bonuses in their own right, not rewards or hit scenarios.
for data in [["kinetic_absorption", "Absorpcja kinetyczna"], ["energy_absorption", "Absorpcja energetyczna"], ["radiation_absorption", "Absorpcja radiacyjna"], ["damage_percent", "Obrażenia broni"], ["critical_damage_percent", "Obrażenia krytyczne"], ["critical_chance_percent", "Szansa na krytyk"], ["shield_on_damage_percent", "Odzysk tarcz z obrażeń"], ["experience_percent", "Doświadczenie"], ["credits_percent", "Kredyty"]]:
stats[data[0]] = {"value": float(bonuses.get(data[0], 0.0)), "name": data[1], "unit": "%"}
stats["luck_percent"] = {"value": GameState.captain.get_luck_chance(float(bonuses.get("luck_percent", 0.0))) * 100.0, "name": "Szczęście (limit 60%)", "unit": "%"}
stats["missile_cooldown_reduction"] = {"value": float(bonuses.get("missile_cooldown_reduction", 0.0)), "name": "Skrócenie cooldownu rakiet", "unit": "s"}
var absorption_values := DamageTypes.capped(Vector3(float(bonuses.get("kinetic_absorption", 0.0)), float(bonuses.get("energy_absorption", 0.0)), float(bonuses.get("radiation_absorption", 0.0))))
for index in 3:
stats[DamageTypes.KEYS[index]]["value"] = absorption_values[index]
return stats
func get_weapon_comparison(item: ItemInstance, slot_index: int) -> Dictionary:
if item == null or item.definition == null or item.definition.category != ItemDefinition.Category.WEAPON or slot_index < 0 or slot_index >= weapon_slots.size():
return {}
var equipped := weapon_slots[slot_index]
var before := _comparison_bonuses()
var after := _comparison_bonuses(item, equipped)
var rows := get_item_comparison(item, equipped)
var flat_damage := _weapon_flat_damage_addons(item) - _weapon_flat_damage_addons(equipped)
if not is_zero_approx(flat_damage):
rows.append({"key": "flat_damage_addon", "name": "Obrażenia z addonów", "delta": flat_damage, "unit": "DMG"})
return {"slot": slot_index, "dps_delta": _weapon_dps(item, after) - _weapon_dps(equipped, before), "stats": rows}
func _weapon_flat_damage_addons(item: ItemInstance) -> float:
var total := 0.0
if item != null:
for modifier in item.additional_modifiers:
if modifier != null and modifier.stat_key == "damage":
total += modifier.value
return total
func _weapon_dps(item: ItemInstance, bonuses: Dictionary) -> float:
if item == null or item.definition == null:
return 0.0
var damage_factor := (1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0) * (1.0 + GameState.captain.get_skill_bonus("gunner"))
var chance := clampf(GameState.captain.get_skill_bonus("sharp_eye") + float(bonuses.get("critical_chance_percent", 0.0)) / 100.0, 0.0, 1.0)
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))
var interval := item.definition.fire_interval
if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE:
interval = maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0)))
return item.get_stat_total("damage") * damage_factor * (1.0 + chance * (critical_factor - 1.0)) / maxf(0.001, interval)
func consume_irradiated_ammunition() -> bool:
if irradiated_ammunition <= 0:
return false
if GameState.captain.should_consume_ammunition("irradiated"):
irradiated_ammunition -= 1
ammunition_changed.emit()
return true
func can_craft_irradiated_ammunition() -> bool:
return can_craft("irradiated_ammunition")
func craft_irradiated_ammunition() -> bool:
return craft("irradiated_ammunition")
func can_buy_irradiated_ammunition() -> bool:
return GameState.captain.credits >= get_irradiated_ammunition_buy_cost()
func get_irradiated_ammunition_buy_cost() -> int:
return GameState.captain.discounted_price(IRRADIATED_AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level))
func buy_irradiated_ammunition() -> bool:
if not can_buy_irradiated_ammunition():
return false
GameState.captain.credits -= get_irradiated_ammunition_buy_cost()
irradiated_ammunition += 10
GameState.captain.emit_changed()
ammunition_changed.emit()
return true
func get_total_dps() -> float:
var total := 0.0
var bonuses := _comparison_bonuses()
for item in weapon_slots:
var value := _weapon_dps(item, bonuses)
if item != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA:
value *= 1.0 + 0.25 * minf(3.0, item.definition.fire_interval)
total += value
return total