This commit is contained in:
Tomasz Boruc 2026-10-11 13:26:14 +02:00
parent 6658adf389
commit 1ad22be08e
16 changed files with 187 additions and 58 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
export_presets.cfg
Randoloot.console.exe
Randoloot.exe
/builds/

24
EXPORT_WINDOWS.md Normal file
View File

@ -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

BIN
assets/Ships/alien_ship.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)")

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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