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 CARGO_ROW_SIZE := 5 var unlocked_cargo_slots := CARGO_ROW_SIZE 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 CARGO_SLOT_COST := {"iron": 100, "aluminium": 50} const WEAPON_UNLOCK_COSTS := [30, 200] const EXTENSION_UNLOCK_BASE_COST := 40 var unlocked_weapon_slots := 1 var unlocked_extension_slots := 1 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 := 200 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 selected_orb: 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: process_mode = Node.PROCESS_MODE_ALWAYS 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: unlocked_cargo_slots = clampi(int(GameState.restart_inventory.get("unlocked_cargo_slots", int(GameState.restart_inventory.get("unlocked_cargo_rows", 1)) * CARGO_ROW_SIZE)), CARGO_ROW_SIZE, CARGO_SIZE) unlocked_weapon_slots = clampi(int(GameState.restart_inventory.get("unlocked_weapon_slots",1)),1,WEAPON_SLOT_COUNT) unlocked_extension_slots = clampi(int(GameState.restart_inventory.get("unlocked_extension_slots",1)),1,EXTENSION_SLOT_COUNT) 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() migrate_orbs() _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 = { "unlocked_cargo_slots": unlocked_cargo_slots, "unlocked_weapon_slots": unlocked_weapon_slots, "unlocked_extension_slots": unlocked_extension_slots, "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(amount: int = 1) -> bool: if amount <= 0 or ammunition < amount: return false if GameState.captain.should_consume_ammunition("ordinary"): ammunition -= amount 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 := RECIPES.output_amount(recipe_id, GameState.captain) 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: if not is_slot_unlocked(slot_kind,index): return null 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 not item.definition.orb_id.is_empty(): for existing in cargo.slice(0,get_cargo_capacity()): if existing != null and existing.definition.orb_id == item.definition.orb_id: existing.stack_count += item.stack_count inventory_changed.emit() return true 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 get_cargo_capacity(): if cargo[index] == null: cargo[index] = item inventory_changed.emit() return true return false func can_add_item(item: ItemInstance) -> bool: if item == null or item.definition == null: return false if item.definition.item_id == "energy_bank": return true for existing in cargo.slice(0,get_cargo_capacity()): if existing == null: return true if not item.definition.orb_id.is_empty() and existing.definition.orb_id == item.definition.orb_id: return true return false func migrate_orbs() -> void: var progress: Resource = GameState.captain.expedition for id in progress.orbs.keys(): var amount := int(progress.orbs[id]) if amount > 0 and add_item(preload("res://scripts/items/special_orbs.gd").create_item(id,amount)): progress.orbs.erase(id) func select_orb(index: int) -> bool: var item := get_item("cargo",index) if item == null or item.definition.orb_id.is_empty(): return false selected_orb = item Input.set_default_cursor_shape(Input.CURSOR_CROSS) if get_node_or_null("OrbTargetCursor") == null: var cursor := CanvasLayer.new() cursor.set_script(preload("res://scripts/ui/orb_target_cursor.gd")) cursor.inventory = self add_child(cursor) inventory_changed.emit() return true func cancel_orb() -> void: selected_orb = null Input.set_default_cursor_shape(Input.CURSOR_ARROW) inventory_changed.emit() func use_selected_orb(item: ItemInstance) -> bool: if selected_orb == null or not cargo.has(selected_orb): cancel_orb() return false if not preload("res://scripts/items/special_orbs.gd").apply(GameState.captain.expedition,self,item,selected_orb.definition.orb_id): return false cancel_orb() return true func _input(event: InputEvent) -> void: if selected_orb == null: return if (event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE) or (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT): cancel_orb() get_viewport().set_input_as_handled() func delete_cargo_item(index: int) -> bool: if not is_slot_unlocked("cargo",index): return false if index < 0 or index >= cargo.size() or cargo[index] == null: return false if not cargo[index].definition.orb_id.is_empty(): 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: if not is_slot_unlocked(from_kind,from_index): return false 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 not is_slot_unlocked(kind,index): return false 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 is_slot_unlocked(kind: String, index: int) -> bool: if kind == "cargo": return index >= 0 and index < get_cargo_capacity() if kind == "weapon": return index >= 0 and index < unlocked_weapon_slots if kind == "extension": return index >= 0 and index < unlocked_extension_slots return true func get_cargo_capacity() -> int: return unlocked_cargo_slots func can_use_freighter_storage(captain: Resource) -> bool: return bool(ship.get("is_docked")) and captain.expedition.freighter_rooms.get("expansion_3","") == "module_storage" func store_on_freighter(index: int, expected: ItemInstance, captain: Resource) -> bool: if not can_use_freighter_storage(captain) or not is_slot_unlocked("cargo",index): return false var item := get_item("cargo",index) if item == null or item != expected: return false var merged := false if not item.definition.orb_id.is_empty(): for existing in captain.expedition.freighter_storage: if existing.definition.orb_id == item.definition.orb_id: existing.stack_count += item.stack_count merged = true break if not merged: captain.expedition.freighter_storage.append(item) cargo[index] = null captain.expedition.emit_changed() inventory_changed.emit() return true func retrieve_from_freighter(index: int, expected: ItemInstance, captain: Resource) -> bool: if not can_use_freighter_storage(captain): return false var storage: Array = captain.expedition.freighter_storage if index < 0 or index >= storage.size() or expected == null or storage[index] != expected: return false if not can_add_item(expected): return false if not add_item(expected): return false storage.remove_at(index) captain.expedition.emit_changed() inventory_changed.emit() return true func _exit_tree() -> void: if selected_orb != null: Input.set_default_cursor_shape(Input.CURSOR_ARROW) func get_slot_unlock_cost(kind: String) -> Dictionary: match kind: "cargo": return CARGO_SLOT_COST.duplicate() if unlocked_cargo_slots < CARGO_SIZE else {} "weapon": return {"titanium": WEAPON_UNLOCK_COSTS[unlocked_weapon_slots - 1]} if unlocked_weapon_slots < WEAPON_SLOT_COUNT else {} "extension": if unlocked_extension_slots >= EXTENSION_SLOT_COUNT: return {} var amount := EXTENSION_UNLOCK_BASE_COST * (1 << (unlocked_extension_slots - 1)) return {"iron": amount, "aluminium": amount, "titanium": amount} return {} func get_slot_unlock_cost_text(kind: String) -> String: var descriptions := PackedStringArray() var cost := get_slot_unlock_cost(kind) for mineral in cost: descriptions.append("%d %s" % [cost[mineral], RECIPES.NAMES[mineral]]) return " + ".join(descriptions) func can_unlock_slot(kind: String, captain: Resource) -> bool: if not bool(ship.get("is_docked")): return false var cost := get_slot_unlock_cost(kind) if cost.is_empty(): return false for mineral in cost: if int(captain.minerals.get(mineral, 0)) < int(cost[mineral]): return false return true func unlock_slot(kind: String, captain: Resource) -> bool: if not can_unlock_slot(kind, captain): return false var cost := get_slot_unlock_cost(kind) for mineral in cost: captain.minerals[mineral] = int(captain.minerals.get(mineral, 0)) - int(cost[mineral]) match kind: "cargo": unlocked_cargo_slots += 1 "weapon": unlocked_weapon_slots += 1 "extension": unlocked_extension_slots += 1 captain.emit_changed() inventory_changed.emit() return true 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.slice(0,unlocked_weapon_slots) + module_slots: if equipped != null and equipped != item: reduction += equipped.get_stat_total("missile_cooldown_reduction") return maxf(1.0, maxf(1.0, 5.0 - reduction) / item.attack_speed_multiplier() * (1.0 - GameState.captain.get_skill_bonus("missile_specialist"))) 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] if displaced != null and not source[from_index].definition.orb_id.is_empty() and displaced.definition.orb_id == source[from_index].definition.orb_id: displaced.stack_count += source[from_index].stack_count source[from_index] = null inventory_changed.emit() return true 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.slice(0,unlocked_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.slice(0,unlocked_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.slice(0,unlocked_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 preload("res://scripts/items/equipment_balance.gd").bonuses(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: bonuses = preload("res://scripts/items/equipment_balance.gd").bonuses(bonuses) 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]} stats.max_speed.value = minf(240.0,float(stats.max_speed.value)) if preload("res://scripts/planets/alien_research.gd").learned("hull_b"): stats.max_hull.value *= 1.1 if preload("res://scripts/planets/alien_research.gd").learned("shields_b"): stats.max_shields.value *= 1.1 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 balance := preload("res://scripts/items/equipment_balance.gd") stats.energy_regeneration.value = balance.energy_regen(float(stats.energy_regeneration.value),float(stats.max_energy.value)) stats.shield_regeneration.value = balance.shield_regen(float(stats.shield_regeneration.value),float(stats.max_shields.value)) # 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 bonuses = preload("res://scripts/items/equipment_balance.gd").bonuses(bonuses) 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, 0.60) 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.get_fire_interval() if item.definition.weapon_effect == "external_turret": interval = item.get_turret_fire_interval() if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: interval = maxf(1.0, maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) / item.attack_speed_multiplier() * (1.0 - GameState.captain.get_skill_bonus("missile_specialist"))) var value := item.get_stat_total("damage") * damage_factor * (1.0 + chance * (critical_factor - 1.0)) / maxf(0.001, interval) if item.definition.weapon_effect == "radiation_laser" and item.get_damage_type() == DamageTypes.Type.RADIATION: value *= 1.0+minf(preload("res://scripts/game/radiation_status.gd").DURATION,interval) if item.definition.weapon_effect == "incendiary": value *= 6.5 return value 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_weapon_dps(slot_index: int) -> float: if not is_slot_unlocked("weapon", slot_index): return 0.0 var item := get_item("weapon", slot_index) var value := _weapon_dps(item, _comparison_bonuses()) if item != null and item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA and item.get_damage_type() == DamageTypes.Type.RADIATION: value *= 1.0 + 0.25 * minf(preload("res://scripts/game/radiation_status.gd").DURATION, item.get_fire_interval()) return value func get_total_dps() -> float: var total := 0.0 for index in range(unlocked_weapon_slots): total += get_weapon_dps(index) return total