diff --git a/.gitignore b/.gitignore index 63db153..41963c2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ export_presets.cfg Randoloot.console.exe Randoloot.exe +/builds/ diff --git a/EXPORT_WINDOWS.md b/EXPORT_WINDOWS.md new file mode 100644 index 0000000..71c3c07 --- /dev/null +++ b/EXPORT_WINDOWS.md @@ -0,0 +1,24 @@ +# Eksport do jednego EXE + +1. W Godocie zainstaluj Export Templates zgodne z wersją edytora (obecnie 4.7.2). +2. Otwórz Project → Export i wybierz preset Randoloot / Windows Desktop. +3. Ustaw architekturę x86_64, Embed PCK włączone i Export Console Wrapper wyłączone. +4. Użyj Export Project i wyłącz Export With Debug. Docelowa ścieżka: `builds/windows/Randoloot.exe`. + +Do przekazania graczowi wystarczy ten EXE. Zasoby są wbudowane. Godot nie musi być zainstalowany na komputerze gracza; potrzebny jest Windows 64-bit i sterownik graficzny obsługujący Vulkan. Projekt używa Vulkan zamiast Direct3D 12, aby eksport nie potrzebował dodatkowych bibliotek Agility SDK / PIX. + +Folder `builds` jest pomijany przez Godota i Git. Lokalny `export_presets.cfg` jest już ignorowany przez Git; przy przenoszeniu projektu należy odtworzyć powyższe ustawienia eksportu. + +## Ścieżki i zasoby + +- W PCK wielkość liter ma znaczenie. Ścieżka musi dokładnie odpowiadać nazwie na dysku, np. `res://assets/Ships/Spaceship_0.png`. +- Importowane grafiki i dźwięki wczytuj przez `load()` / `preload()` / ResourceLoader. Surowy FileAccess nie gwarantuje dostępu do oryginalnego pliku po eksporcie. +- Dane zapisywane podczas gry powinny korzystać z `user://`, ponieważ `res://` w EXE jest tylko do odczytu. +- Po eksporcie sprawdź EXE w osobnym folderze bez plików projektu. + +## Dokumentacja + +- https://docs.godotengine.org/en/stable/tutorials/export/exporting_for_windows.html +- https://docs.godotengine.org/en/stable/classes/class_editorexportplatformwindows.html +- https://docs.godotengine.org/en/stable/classes/class_fileaccess.html +- https://docs.godotengine.org/en/stable/tutorials/best_practices/project_organization.html diff --git a/assets/Ships/alien_ship.jpg b/assets/Ships/alien_ship.jpg new file mode 100644 index 0000000..9b5bd91 Binary files /dev/null and b/assets/Ships/alien_ship.jpg differ diff --git a/project.godot b/project.godot index f39289a..88ff5ea 100644 --- a/project.godot +++ b/project.godot @@ -15,10 +15,6 @@ run/main_scene="res://scenes/ui/main_menu.tscn" config/features=PackedStringArray("4.7", "Forward Plus") config/icon="res://icon.svg" -[audio] - -buses/default_bus_layout="res://default_bus_layout.tres" - [autoload] GameState="*res://scripts/game/game_state.gd" @@ -88,7 +84,3 @@ ship_fire_secondary={ 3d/physics_engine="Jolt Physics" common/physics_interpolation=true - -[rendering] - -rendering_device/driver.windows="d3d12" diff --git a/resources/items/machine_gun.tres b/resources/items/machine_gun.tres index b2648e2..9b345a9 100644 --- a/resources/items/machine_gun.tres +++ b/resources/items/machine_gun.tres @@ -7,7 +7,7 @@ script = ExtResource("1_definition") item_id = "machine_gun" display_name = "Działko maszynowe" category = 0 -description = "Bardzo szybkostrzelna broń kinetyczna." +description = "Bardzo szybkostrzelna broń kinetyczna. Zużywa 1 sztukę amunicji na strzał." primary_stat = "damage" primary_value = 4.0 stat_suffix = " DMG" @@ -15,7 +15,7 @@ weapon_kind = 1 fire_interval = 0.1 projectile_speed = 1150.0 weapon_range = 550.0 -energy_cost = 3.0 +energy_cost = 0.0 damage_lower_roll_min = 2 damage_lower_roll_max = 4 damage_spread_roll_min = 1 diff --git a/scenes/ui/planet_services_menu.tscn b/scenes/ui/planet_services_menu.tscn index 8d65c42..0219ab2 100644 --- a/scenes/ui/planet_services_menu.tscn +++ b/scenes/ui/planet_services_menu.tscn @@ -71,10 +71,16 @@ layout_mode = 2 size_flags_vertical = 3 theme_override_colors/font_color = Color(0.59, 0.67, 0.75, 1) theme_override_font_sizes/font_size = 14 -text = "Usługi w przygotowaniu" +text = "Kup amunicję do działka maszynowego." horizontal_alignment = 1 vertical_alignment = 1 +[node name="BuyAmmunitionButton" type="Button" parent="Center/Panel/Contents"] +custom_minimum_size = Vector2(0, 44) +layout_mode = 2 +theme_override_font_sizes/font_size = 15 +text = "KUP 100 AMUNICJI — 100 KREDYTÓW" + [node name="NextButton" type="Button" parent="Center/Panel/Contents"] custom_minimum_size = Vector2(0, 44) layout_mode = 2 diff --git a/scenes/ui/ship_inventory_panel.tscn b/scenes/ui/ship_inventory_panel.tscn index 06b6061..67f8ba8 100644 --- a/scenes/ui/ship_inventory_panel.tscn +++ b/scenes/ui/ship_inventory_panel.tscn @@ -49,6 +49,22 @@ layout_mode = 2 theme_override_font_sizes/font_size = 11 text = "ZAMKNIJ" +[node name="Ammunition" type="HBoxContainer" parent="Contents"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Count" type="Label" parent="Contents/Ammunition"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 11 +text = "AMUNICJA: 300" + +[node name="CraftButton" type="Button" parent="Contents/Ammunition"] +layout_mode = 2 +theme_override_font_sizes/font_size = 11 +text = "TWÓRZ +100 AMUNICJI (50 części)" +tooltip_text = "Zamień 50 części na 100 sztuk amunicji do działka maszynowego." + [node name="CargoGrid" type="GridContainer" parent="Contents"] layout_mode = 2 size_flags_horizontal = 4 diff --git a/scripts/game/scene_music_player.gd b/scripts/game/scene_music_player.gd index 50b69d9..0d5b031 100644 --- a/scripts/game/scene_music_player.gd +++ b/scripts/game/scene_music_player.gd @@ -8,15 +8,11 @@ func _ready() -> void: push_error("SceneMusicPlayer has no track_path configured.") return - var audio_data := FileAccess.get_file_as_bytes(track_path) - if audio_data.is_empty(): - push_error("Could not read music track: %s" % track_path) - return - - var music_stream := AudioStreamMP3.load_from_buffer(audio_data) + var music_stream := load(track_path) as AudioStreamMP3 if music_stream == null: push_error("Could not decode music track: %s" % track_path) return + music_stream = music_stream.duplicate() as AudioStreamMP3 music_stream.loop = true stream = music_stream volume_db = playback_volume_db diff --git a/scripts/game/ship_engine_audio.gd b/scripts/game/ship_engine_audio.gd index 88d56ba..9ab7795 100644 --- a/scripts/game/ship_engine_audio.gd +++ b/scripts/game/ship_engine_audio.gd @@ -9,15 +9,11 @@ var _ship: CharacterBody2D func _ready() -> void: _ship = get_parent() as CharacterBody2D - var audio_data := FileAccess.get_file_as_bytes(ENGINE_SOUND_PATH) - if audio_data.size() <= 44: - push_error("Could not read engine sound: %s" % ENGINE_SOUND_PATH) - return - - var engine_stream := AudioStreamMP3.load_from_buffer(audio_data) + var engine_stream := load(ENGINE_SOUND_PATH) as AudioStreamMP3 if engine_stream == null: push_error("Could not decode engine sound: %s" % ENGINE_SOUND_PATH) return + engine_stream = engine_stream.duplicate() as AudioStreamMP3 engine_stream.loop = true stream = engine_stream volume_db = SILENT_VOLUME_DB diff --git a/scripts/game/weapon_audio.gd b/scripts/game/weapon_audio.gd index e143a23..f951046 100644 --- a/scripts/game/weapon_audio.gd +++ b/scripts/game/weapon_audio.gd @@ -25,7 +25,7 @@ static func play(parent: Node, source_position: Vector2, weapon_kind: int, volum return if not _streams.has(weapon_kind): var sound_path: String = SOUND_PATHS[weapon_kind] - _streams[weapon_kind] = AudioStreamMP3.load_from_buffer(FileAccess.get_file_as_bytes(sound_path)) + _streams[weapon_kind] = load(sound_path) as AudioStreamMP3 var stream := _streams[weapon_kind] as AudioStreamMP3 if stream == null: return diff --git a/scripts/game/weapon_controller.gd b/scripts/game/weapon_controller.gd index da26b84..4a26455 100644 --- a/scripts/game/weapon_controller.gd +++ b/scripts/game/weapon_controller.gd @@ -107,7 +107,7 @@ func _physics_process(delta: float) -> void: _fire_projectile(item) _shot_timers[slot_index] = definition.weapon_cooldown _: - if _shot_timers[slot_index] <= 0.0 and _consume_energy(item.get_energy_cost()): + if _shot_timers[slot_index] <= 0.0 and _consume_shot_resource(item): _fire_projectile(item) _shot_timers[slot_index] = definition.fire_interval for slot_index in 2: @@ -173,6 +173,11 @@ func _muzzle_position() -> Vector2: var forward := Vector2.UP.rotated(ship.rotation) return ship.global_position + forward * 28.0 +func _consume_shot_resource(item: ItemInstance) -> bool: + if item.definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + return inventory.consume_ammunition() + return _consume_energy(item.get_energy_cost()) + func _consume_energy(amount: float) -> bool: var current_energy := float(ship.get("energy")) if current_energy < amount: diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index c8469cf..77e2799 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -53,6 +53,8 @@ func roll_damage() -> float: func get_energy_cost() -> float: if definition == null or definition.category != ItemDefinition.Category.WEAPON: return 0.0 + if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + return 0.0 # Compare this weapon with its starter damage. Captain/global gear buffs are free. var base_lower := roundf((definition.damage_lower_roll_min + definition.damage_lower_roll_max) * 0.5) var base_spread := roundf((definition.damage_spread_roll_min + definition.damage_spread_roll_max) * 0.5) @@ -109,7 +111,10 @@ func get_tooltip_text() -> String: 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, get_energy_cost()]) + if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + lines.append("Zasięg: %.0f px • Amunicja: 1 / strzał" % definition.weapon_range) + else: + lines.append("Zasięg: %.0f px • Energia: %.1f / strzał" % [definition.weapon_range, get_energy_cost()]) lines.append("Odstęp strzałów: %.2f s" % maxf(definition.fire_interval, definition.weapon_cooldown)) if not additional_modifiers.is_empty(): lines.append("\nADDONY (aktywne po zamontowaniu)") diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index 8ca35a4..bf1a8eb 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -2,10 +2,18 @@ extends Node class_name ShipInventory signal inventory_changed +signal ammunition_changed const CARGO_SIZE := 20 const WEAPON_SLOT_COUNT := 2 const MODULE_SLOT_COUNT := 6 +const STARTING_AMMUNITION := 300 +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 cargo: Array[ItemInstance] = [] var weapon_slots: Array[ItemInstance] = [] @@ -24,6 +32,7 @@ func _ready() -> void: cargo.assign(GameState.restart_inventory["cargo"]) weapon_slots.assign(GameState.restart_inventory["weapon"]) module_slots.assign(GameState.restart_inventory["module"]) + ammunition = int(GameState.restart_inventory.get("ammunition", STARTING_AMMUNITION)) GameState.restart_inventory.clear() _recalculate_ship_stats() inventory_changed.emit() @@ -41,14 +50,49 @@ func preserve_for_restart() -> void: "cargo": cargo.duplicate(), "weapon": weapon_slots.duplicate(), "module": module_slots.duplicate(), + "ammunition": 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 @@ -122,34 +166,11 @@ func _recalculate_ship_stats() -> void: ship.call("apply_equipment_bonuses", bonuses) func _add_starter_items() -> void: - var starter_items := [ - "res://resources/items/plasma_cannon.tres", - "res://resources/items/beam_laser.tres", - "res://resources/items/machine_gun.tres", - "res://resources/items/missile_launcher.tres", - "res://resources/items/reinforced_shield.tres", - "res://resources/items/hull_plating.tres", - "res://resources/items/compact_generator.tres", - "res://resources/items/engine_extension.tres", - "res://resources/items/tuned_thrusters.tres", - "res://resources/items/shield_recharger.tres", - "res://resources/items/afterburner.tres", - "res://resources/items/shield_charge.tres", - "res://resources/items/repair_drones.tres", - ] - var cargo_index := 0 - var weapon_index := 0 - for path in starter_items: - var definition := load(path) as ItemDefinition - if definition == null: - push_warning("Could not load starter item: %s" % path) - continue - var item := ItemInstance.new() - item.definition = definition - item.item_level = 1 - if definition.category == ItemDefinition.Category.WEAPON and weapon_index < WEAPON_SLOT_COUNT: - weapon_slots[weapon_index] = item - weapon_index += 1 - else: - cargo[cargo_index] = item - cargo_index += 1 + 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 diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd index 5a047f3..6e4f3fe 100644 --- a/scripts/ui/planet_services_menu.gd +++ b/scripts/ui/planet_services_menu.gd @@ -5,16 +5,37 @@ signal closed @onready var next_button: Button = $Center/Panel/Contents/NextButton @onready var close_button: Button = $Center/Panel/Contents/CloseButton +@onready var buy_ammunition_button: Button = $Center/Panel/Contents/BuyAmmunitionButton +@onready var services_label: Label = $Center/Panel/Contents/Services + +var _inventory: ShipInventory func _ready() -> void: next_button.pressed.connect(func() -> void: next_level_requested.emit()) close_button.pressed.connect(_close) + buy_ammunition_button.pressed.connect(_buy_ammunition) + _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory + if _inventory: + _inventory.ammunition_changed.connect(_refresh_ammunition_service) + GameState.captain.changed.connect(_refresh_ammunition_service) + _refresh_ammunition_service() func show_services(at_level_cap: bool) -> void: + _refresh_ammunition_service() next_button.disabled = at_level_cap next_button.text = "OSIĄGNIĘTO OSTATNI UKŁAD" if at_level_cap else "PRZEJDŹ DO NASTĘPNEGO UKŁADU" show() +func _refresh_ammunition_service() -> void: + buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition() + if _inventory: + buy_ammunition_button.text = "KUP %d AMUNICJI — %d KREDYTÓW" % [ShipInventory.AMMUNITION_BUY_AMOUNT, _inventory.get_ammunition_buy_cost()] + services_label.text = "Amunicja: %d • Kredyty: %d\nCena: 100 kredytów × poziom układu (%d)" % [_inventory.ammunition, GameState.captain.credits, GameState.system_level] + +func _buy_ammunition() -> void: + if visible and _inventory: + _inventory.buy_ammunition() + func _close() -> void: hide() closed.emit() diff --git a/scripts/ui/ship_inventory_panel.gd b/scripts/ui/ship_inventory_panel.gd index ebda3c6..bc60fd0 100644 --- a/scripts/ui/ship_inventory_panel.gd +++ b/scripts/ui/ship_inventory_panel.gd @@ -6,6 +6,8 @@ const CARGO_SIZE := 20 @onready var cargo_grid: GridContainer = $Contents/CargoGrid @onready var close_button: Button = $Contents/Header/CloseButton @onready var title_label: Label = $Contents/Header/Title +@onready var ammunition_label: Label = $Contents/Ammunition/Count +@onready var craft_button: Button = $Contents/Ammunition/CraftButton var _inventory: ShipInventory var _drawer_open := false @@ -14,9 +16,12 @@ var drawer_width := 780.0 func _ready() -> void: close_button.pressed.connect(close_drawer) + craft_button.pressed.connect(_craft_ammunition) + craft_button.text = "TWÓRZ +%d AMUNICJI (%.0f części)" % [ShipInventory.AMMUNITION_CRAFT_AMOUNT, ShipInventory.AMMUNITION_CRAFT_PARTS_COST] _inventory = get_tree().get_first_node_in_group("ship_inventory") as ShipInventory if _inventory: _inventory.inventory_changed.connect(_refresh_title) + _inventory.ammunition_changed.connect(_refresh_title) GameState.captain.changed.connect(_refresh_title) for index in CARGO_SIZE: var slot := SLOT_SCENE.instantiate() @@ -26,8 +31,14 @@ func _ready() -> void: _refresh_title() func _refresh_title() -> void: + craft_button.disabled = _inventory == null or not _inventory.can_craft_ammunition() if _inventory: title_label.text = "MAGAZYN (%d/%d) · Części: %.1f" % [_inventory.get_cargo_used(), CARGO_SIZE, GameState.captain.parts] + ammunition_label.text = "AMUNICJA: %d" % _inventory.ammunition + +func _craft_ammunition() -> void: + if _inventory: + _inventory.craft_ammunition() func toggle_drawer() -> void: _set_drawer_open(not _drawer_open) diff --git a/tests/items/weapon_damage_test.gd b/tests/items/weapon_damage_test.gd index af71215..9dc6d86 100644 --- a/tests/items/weapon_damage_test.gd +++ b/tests/items/weapon_damage_test.gd @@ -32,7 +32,10 @@ func _ready() -> void: addon.stat_key = "damage_percent" addon.value = 10.0 stronger.additional_modifiers.append(addon) - assert(stronger.get_energy_cost() > cost_before_addon) + if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + assert(stronger.get_energy_cost() == 0.0) + else: + assert(stronger.get_energy_cost() > cost_before_addon) for level in [1, 50, 1000]: for sample in 100: var item := GENERATOR.generate_for_definition(definition, level) @@ -73,6 +76,38 @@ func _ready() -> void: var probe := LaserProbe.new() player.add_child(probe) probe.set_physics_process(false) + var inventory := player.get_node("ShipInventory") as ShipInventory + assert(inventory.ammunition == 300) + var machine_gun := ItemInstance.new() + machine_gun.definition = preload("res://resources/items/machine_gun.tres") + player.set("energy", 0.0) + inventory.ammunition = 1 + assert(probe._consume_shot_resource(machine_gun)) + assert(inventory.ammunition == 0 and player.get("energy") == 0.0) + assert(not probe._consume_shot_resource(machine_gun)) + GameState.captain.parts = 49.0 + assert(not inventory.craft_ammunition()) + assert(inventory.ammunition == 0 and GameState.captain.parts == 49.0) + GameState.captain.parts = 50.0 + assert(inventory.craft_ammunition()) + assert(inventory.ammunition == 100 and GameState.captain.parts == 0.0) + GameState.system_level = 3 + assert(inventory.get_ammunition_buy_cost() == 300) + GameState.captain.credits = 299 + assert(not inventory.buy_ammunition()) + assert(inventory.ammunition == 100 and GameState.captain.credits == 299) + GameState.captain.credits = 600 + assert(inventory.buy_ammunition()) + assert(inventory.ammunition == 200 and GameState.captain.credits == 300) + assert(inventory.buy_ammunition()) + assert(inventory.ammunition == 300 and GameState.captain.credits == 0) + assert(not inventory.buy_ammunition()) + GameState.system_level = 1 + inventory.preserve_for_restart() + var restored_player := PLAYER_SCENE.instantiate() as CharacterBody2D + add_child(restored_player) + assert(restored_player.get_node("ShipInventory").get("ammunition") == 300) + restored_player.queue_free() var target := StaticBody2D.new() target.position = Vector2(200, -28) target.collision_layer = 4