Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,21 +132,10 @@ these GPL requirements, a separate license is available upon request.

---

### Play Instructions

- **In Godot Editor**: Open the project > Press F5 to run the main scene
(`main_menu.tscn`). Use keyboard/mouse for navigation.
- **Local Web Testing**: After exporting to HTML5 (see Setup),
cd to `infra/` in `PowerShell` > `docker compose up -d` >
Open <http://localhost:9090> in browser. Click "Run game" iframe;
use fullscreen for immersion (ignore desktop console warnings).
- **Online on itch.io**:
1. Visit [ikostan.itch.io/sky-lock-assault](https://ikostan.itch.io/sky-lock-assault)
2. Click "Run game".
- The game supports browser play; mobile-friendly with landscape orientation.
## 🟡 Current Development Status

For learning: Test quit handling (platform-specific) and log levels in options
menu during play.
**Milestone:** UI & Input Improvements, Testing & Docs (Milestone 10)
**Active Focus:** Input remapping tests, Settings + UI sync, documentation updates.

### Current features

Expand Down Expand Up @@ -186,8 +175,6 @@ menu during play.
caution, red/dark red danger), and low/over-speed blinking warnings
(PR #275 and #288).
- Rotor SFX/animation with volume sliders and zero-fuel stopping (prior PRs).

- **In Progress (Milestone 8: Advanced Features, Maintenance and Bug Fixes)**:
- Refactor player movement integrations
(e.g., speed-based fuel drain, UI sync) – Issue #169.
- Add procedural random parallax background for speed-based
Expand Down Expand Up @@ -224,7 +211,27 @@ Track progress via [Milestones](https://github.com/ikostan/SkyLockAssault/milest
- Placeholder level lacks mechanics—work in progress.
- Report new issues on [GitHub](https://github.com/ikostan/SkyLockAssault/issues).

### How to Contribute
---

### Play Instructions

- **In Godot Editor**: Open the project > Press F5 to run the main scene
(`main_menu.tscn`). Use keyboard/mouse for navigation.
- **Local Web Testing**: After exporting to HTML5 (see Setup),
cd to `infra/` in `PowerShell` > `docker compose up -d` >
Open <http://localhost:9090> in browser. Click "Run game" iframe;
use fullscreen for immersion (ignore desktop console warnings).
- **Online on itch.io**:
1. Visit [ikostan.itch.io/sky-lock-assault](https://ikostan.itch.io/sky-lock-assault)
2. Click "Run game".
- The game supports browser play; mobile-friendly with landscape orientation.

For learning: Test quit handling (platform-specific) and log levels in options
menu during play.

---

### 🙌 How to Contribute

- Fork the repo and create a branch for your changes.
- Follow GDScript best practices; test in editor and web export.
Expand All @@ -233,6 +240,16 @@ Track progress via [Milestones](https://github.com/ikostan/SkyLockAssault/milest
- **Join the Discussions**: Chat about ideas, ask questions on web exports, or suggest
features at [GitHub Discussions](https://github.com/ikostan/SkyLockAssault/discussions).

We use the following labels to organize contributions:

- `good first issue` — beginner friendly
- `documentation` — improvements to docs
- `feature` — gameplay or UI work

Please create a branch per issue and reference the issue number in your PR title.

---

### Contributors

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
Expand Down
Binary file modified requirements.txt
Binary file not shown.
166 changes: 166 additions & 0 deletions test/gut/test_integration_key_mapping.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
## Copyright (C) 2025 Egor Kostan
## SPDX-License-Identifier: GPL-3.0-or-later
## test_integration_key_mapping.gd
## GUT unit tests for integration of key mapping load, remap, persistence, and reset.
## Covers INT-01 to INT-03 from test plan.
## Test Plan:
## https://github.com/ikostan/SkyLockAssault/issues/350
## References: test_key_mapping_menu.gd, test_input_remap_button.gd, input_remap_button.gd, key_mapping.gd
## Assumes Settings.load_input_mappings() loads [input] section from config to InputMap.
## Assumes Settings.save_input_mappings() saves InputMap events to [input] as arrays of strings (e.g., ["key:87"] for KEY_W).
## Assumes Settings.reset_to_defaults(device_type) resets InputMap for that device and saves.
## Uses test config path to isolate; focuses on keyboard for brevity (extend for gamepad if needed).
## Uses "speed_up" as test action (from UI paths); assumes default keyboard is KEY_W ("W").
## Simulates remap via direct calls to private methods due to input event mocking complexity in GUT.

extends GutTest

const InputRemapButton: Script = preload("res://scripts/input_remap_button.gd")
const TEST_ACTION: String = "speed_up" # Example action from UI; adjust if needed.
const TEST_CONFIG_PATH: String = "user://settings.cfg"
const KEY_W_CODE: int = Key.KEY_W # 87, default assumed.
const KEY_Z_CODE: int = Key.KEY_Z # 90, custom for tests.

var menu: CanvasLayer = null


## Per-test setup: Delete test config, reset InputMap for test action.
## :rtype: void
func before_each() -> void:
if FileAccess.file_exists(TEST_CONFIG_PATH):
var err: Error = DirAccess.remove_absolute(TEST_CONFIG_PATH)
assert_eq(err, OK)
for action in Settings.ACTIONS:
if InputMap.has_action(action):
InputMap.action_erase_events(action)
else:
InputMap.add_action(action)
Settings.load_input_mappings() # Reload empty

Comment thread
ikostan marked this conversation as resolved.
Resolved

Comment thread
ikostan marked this conversation as resolved.
Resolved
## Per-test cleanup: Free menu, delete test config.
## :rtype: void
func after_each() -> void:
if is_instance_valid(menu):
menu.queue_free()
if FileAccess.file_exists(TEST_CONFIG_PATH):
var err: Error = DirAccess.remove_absolute(TEST_CONFIG_PATH)
assert_eq(err, OK)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
await get_tree().process_frame


## INT-01 | Load mappings → UI | Pre-saved config | UI shows correct mappings | Matches config file
## :rtype: void
func test_int_01_load_to_ui() -> void:
gut.p("INT-01: Pre-saved config loads to InputMap and UI shows correct labels matching config.")
# Setup: Create config with custom keyboard mapping (e.g., "Z" instead of default "W")
var config: ConfigFile = ConfigFile.new()
config.set_value("input", TEST_ACTION, ["key:" + str(KEY_Z_CODE)]) # Assume string format "key:<code>"
config.save(TEST_CONFIG_PATH)
# Load mappings to InputMap
Settings.load_input_mappings()
# Verify InputMap loaded correctly
var events: Array[InputEvent] = InputMap.action_get_events(TEST_ACTION)
var key_events: Array[InputEventKey] = []
for ev: InputEvent in events:
if ev is InputEventKey:
key_events.append(ev)
assert_eq(key_events.size(), 1, "Should have one keyboard event after load")
var key_ev: InputEventKey = key_events[0]
assert_eq(key_ev.physical_keycode, KEY_Z_CODE, "Loaded keycode should match config")
# Instantiate menu and add to tree (UI updates in _ready via update_button_text)
menu = load("res://scenes/key_mapping_menu.tscn").instantiate()
add_child(menu)
# Get specific remap button for test action (keyboard default)
var speed_up_btn: InputRemapButton = menu.get_node("Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedUp/SpeedUpInputRemap")
assert_not_null(speed_up_btn, "SpeedUp remap button should exist")
# Validation: UI matches config/loaded mapping
assert_eq(speed_up_btn.text, "Z", "UI label should show loaded custom key 'Z'")
# Double-check config unchanged
config = ConfigFile.new()
config.load(TEST_CONFIG_PATH)
var saved_events: Array = config.get_value("input", TEST_ACTION, [])
assert_true(saved_events.any(func(s: String) -> bool: return s == "key:" + str(KEY_Z_CODE)), "Config should match")

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

## INT-02 | Remap + persist | Change action then save | Reload restores mapping | Disk & UI sync
## :rtype: void
func test_int_02_remap_persist() -> void:
gut.p("INT-02: Remap updates InputMap/UI, saves to config; reload restores to disk/UI sync.")
# Setup: Default mapping (assume reset sets to "W"); instantiate menu
Settings.reset_to_defaults("keyboard") # Ensure defaults loaded
menu = load("res://scenes/key_mapping_menu.tscn").instantiate()
add_child(menu)
var speed_up_btn: InputRemapButton = menu.get_node("Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedUp/SpeedUpInputRemap")
assert_eq(speed_up_btn.text, "W", "Should start with default 'W'")
# Simulate remap to "Z" (direct calls as in ref tests)
speed_up_btn.button_pressed = true
speed_up_btn._on_pressed() # Start listening
var temp_event := InputEventKey.new()
temp_event.physical_keycode = KEY_Z_CODE
temp_event.pressed = true
speed_up_btn._input(temp_event) # Triggers erase/add/save
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
# Verify immediate UI/InputMap update
assert_eq(speed_up_btn.text, "Z", "UI label should update to new key 'Z'")
var events: Array[InputEvent] = InputMap.action_get_events(TEST_ACTION)
var key_events: Array[InputEventKey] = []
for ev: InputEvent in events:
if ev is InputEventKey:
key_events.append(ev)
assert_eq(key_events.size(), 1, "Should have one keyboard event after remap")
var key_ev: InputEventKey = key_events[0]
assert_eq(key_ev.physical_keycode, KEY_Z_CODE, "InputMap should have new keycode")
# Verify saved to config (persist)
var config: ConfigFile = ConfigFile.new()
config.load(TEST_CONFIG_PATH)
assert_true(config.has_section("input"), "Config should have input section after save")
assert_true(config.has_section_key("input", TEST_ACTION), "Config should have key after save")
var saved_events: Array = config.get_value("input", TEST_ACTION, [])
assert_true(saved_events.any(func(s: String) -> bool: return s == "key:" + str(KEY_Z_CODE)), "Config should match new mapping")
# Simulate reload: Erase InputMap, load from config, update UI
InputMap.action_erase_events(TEST_ACTION)
Settings.load_input_mappings()
speed_up_btn.update_button_text() # Or reinstantiate, but update suffices
assert_eq(speed_up_btn.text, "Z", "UI should restore loaded mapping after reload")
events = InputMap.action_get_events(TEST_ACTION)
key_events = []
for ev: InputEvent in events:
if ev is InputEventKey:
key_events.append(ev)
assert_eq(key_events.size(), 1, "Should have one keyboard event after reload")
key_ev = key_events[0]
assert_eq(key_ev.physical_keycode, KEY_Z_CODE, "InputMap restored from disk")


## INT-03 | Reset via UI | Click reset | Mappings + config reset | Defaults in UI & file
## :rtype: void
func test_int_03_reset_via_ui() -> void:
gut.p("INT-03: Reset button resets InputMap/config to defaults; UI/file show defaults.")
# Setup: Set custom mapping in config/InputMap, then instantiate menu
var config: ConfigFile = ConfigFile.new()
config.set_value("input", TEST_ACTION, ["key:" + str(KEY_Z_CODE)])
config.save(TEST_CONFIG_PATH)
Settings.load_input_mappings()
menu = load("res://scenes/key_mapping_menu.tscn").instantiate()
add_child(menu)
var speed_up_btn: InputRemapButton = menu.get_node("Panel/Options/KeyMapContainer/PlayerKeyMap/KeyMappingSpeedUp/SpeedUpInputRemap")
assert_eq(speed_up_btn.text, "Z", "Should start with custom 'Z'")
# Get reset button
var reset_btn: Button = menu.get_node("Panel/Options/BtnContainer/ControlResetButton")
# Simulate reset (keyboard mode default)
reset_btn.pressed.emit() # Triggers _on_reset_pressed → Settings.reset_to_defaults("keyboard") → update_all_remap_buttons
# Verify UI/InputMap reset to default
assert_eq(speed_up_btn.text, "W", "UI label should reset to default 'W'")
var events: Array[InputEvent] = InputMap.action_get_events(TEST_ACTION)
var key_events: Array[InputEventKey] = []
for ev: InputEvent in events:
if ev is InputEventKey:
key_events.append(ev)
assert_eq(key_events.size(), 1, "Should have one keyboard event after reset")
var key_ev: InputEventKey = key_events[0]
assert_eq(key_ev.physical_keycode, KEY_W_CODE, "InputMap should reset to default keycode")
# Verify config reset (assume reset saves defaults)
config = ConfigFile.new()
config.load(TEST_CONFIG_PATH)
var saved_events: Array = config.get_value("input", TEST_ACTION, [])
assert_true(saved_events.any(func(s: String) -> bool: return s == "key:" + str(KEY_W_CODE)), "Config should reset to default mapping")
Loading