Compare commits
No commits in common. "ded8494cc603822701001e65629ef44944fef6df" and "cf7a963f18ea8f99230a7261910c0715d106b7a6" have entirely different histories.
ded8494cc6
...
cf7a963f18
50 changed files with 130 additions and 2881 deletions
|
|
@ -71,7 +71,7 @@ detail.
|
||||||
|
|
||||||
## Authorship and assets
|
## Authorship and assets
|
||||||
|
|
||||||
NETfishing uses a mixture of original assets and
|
NETfishing is human-directed and uses a mixture of human-authored assets and
|
||||||
software-development assistance. The project records this plainly rather than
|
software-development assistance. The project records this plainly rather than
|
||||||
claiming that repository history can prove how each line was produced. See
|
claiming that repository history can prove how each line was produced. See
|
||||||
[`CREDITS.md`](CREDITS.md) and
|
[`CREDITS.md`](CREDITS.md) and
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
# PortMaster performance profile
|
|
||||||
|
|
||||||
The PortMaster launcher keeps the normal game profile on unknown and stronger
|
|
||||||
hardware. It enables the low-end profile only when the Linux device tree
|
|
||||||
reports one of the identifiers used by the Allwinner H616/H700 XX handhelds:
|
|
||||||
|
|
||||||
- `allwinner,h616`
|
|
||||||
- `sun50iw9p1`
|
|
||||||
- `allwinner,sun50i-h700`
|
|
||||||
|
|
||||||
The low-end launch profile passes these options to Godot:
|
|
||||||
|
|
||||||
- `--single-window`
|
|
||||||
- `--disable-vsync`
|
|
||||||
- `--max-fps 30`
|
|
||||||
- `--audio-output-latency 40`
|
|
||||||
|
|
||||||
It also passes `NETFISHING_LOW_END=1`. The game responds by rendering the 3D
|
|
||||||
world at 75 percent linear resolution with nearest-neighbor scaling. This
|
|
||||||
reduces 3D pixel work by about 44 percent while the separately rendered UI
|
|
||||||
retains its canonical resolution.
|
|
||||||
|
|
||||||
Do not use Godot's low processor mode for this profile. It reduces idle CPU
|
|
||||||
usage by sleeping between updates and is not a game-performance optimization.
|
|
||||||
|
|
||||||
When adding another device family, record its exact NUL-separated device-tree
|
|
||||||
`compatible` value from the hardware before extending the launcher match. Do
|
|
||||||
not infer detection from a retail product name alone.
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
# PortMaster release builds
|
|
||||||
|
|
||||||
NETfishing's PortMaster release is a canonical PortMaster package, not a
|
|
||||||
version-named generic ZIP. The installer-facing archive must be named
|
|
||||||
`netfishing.zip`, and its root must contain exactly:
|
|
||||||
|
|
||||||
```text
|
|
||||||
NETfishing.sh
|
|
||||||
netfishing/
|
|
||||||
port.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not derive a release package from an older local `portmaster-stage`
|
|
||||||
directory. The templates in `scripts/portmaster/` are authoritative.
|
|
||||||
|
|
||||||
## Release contract
|
|
||||||
|
|
||||||
- `port.json` uses schema version 4 and names `netfishing.zip`.
|
|
||||||
- `NETfishing.sh` starts with `# PORTMASTER: netfishing.zip, NETfishing.sh`.
|
|
||||||
- The executable is `netfishing/NETfishing.aarch64`.
|
|
||||||
- The package declares AArch64, two analog sticks, GLIBC 2.28, and
|
|
||||||
`weston_pkg_0.2.squashfs`.
|
|
||||||
- The launcher must not start GPTOKEYB. Godot and NETfishing's controller
|
|
||||||
mapping manager handle controller input directly.
|
|
||||||
- Persistent device data remains under `netfishing/conf/data`,
|
|
||||||
`netfishing/conf/config`, and `netfishing/conf/cache`.
|
|
||||||
- The archive must not contain `conf/`, saves, identities, logs, source files,
|
|
||||||
`.git`, or `.godot` content.
|
|
||||||
- Release downloads must publish `netfishing.zip`. A renamed versioned ZIP is
|
|
||||||
not a substitute because HarbourMaster identifies the canonical port by
|
|
||||||
archive name.
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
Run this only after the release commit and annotated `v<project-version>` tag
|
|
||||||
are pinned to the same clean `main` commit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/build_portmaster.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The script deletes and regenerates the Linux ARM64 export, stages the package,
|
|
||||||
validates its structure, and writes:
|
|
||||||
|
|
||||||
```text
|
|
||||||
builds/v<project-version>/netfishing.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
`--package-only` is reserved for repackaging an already validated ARM64 export.
|
|
||||||
It does not rebuild game content.
|
|
||||||
|
|
||||||
## Local muOS installation
|
|
||||||
|
|
||||||
The muOS auto-install directory is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/mnt/mmc/MUOS/PortMaster/autoinstall/
|
|
||||||
```
|
|
||||||
|
|
||||||
For an explicit offline installation test, copy `netfishing.zip` to a temporary
|
|
||||||
device directory and run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/mnt/mmc/MUOS/PortMaster/harbourmaster \
|
|
||||||
--offline --no-check install ./netfishing.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
Before upgrading an existing installation, preserve
|
|
||||||
`/mnt/mmc/ports/netfishing/conf/` on the device. After installation:
|
|
||||||
|
|
||||||
1. Confirm the installed executable and PCK hashes match the staged release.
|
|
||||||
2. Confirm the installed launcher contains the canonical PortMaster header and
|
|
||||||
does not contain `GPTOKEYB`.
|
|
||||||
3. Confirm `conf/` was not replaced or removed.
|
|
||||||
4. Launch the installed port through the normal muOS menu.
|
|
||||||
5. Verify the displayed game version and controller face-button mapping.
|
|
||||||
|
|
||||||
Installing the public PortMaster catalog entry can still install an older
|
|
||||||
catalog build until the upstream `netfishing.zip` is updated. A local release
|
|
||||||
test must install the generated local archive explicitly.
|
|
||||||
|
|
||||||
## Template provenance
|
|
||||||
|
|
||||||
The current catalog screenshot is a 640x480 PNG copied
|
|
||||||
byte-for-byte into the package template:
|
|
||||||
|
|
||||||
```text
|
|
||||||
f24daae48f543b3a270a5aa46b5e73a31904bf0c7216d4c8a9e15a8dc48a5eed screenshot.png
|
|
||||||
```
|
|
||||||
|
|
||||||
The remaining catalog metadata assets were copied from the installed official
|
|
||||||
`netfishing.zip` package and are intentionally preserved byte-for-byte:
|
|
||||||
|
|
||||||
```text
|
|
||||||
e2a9d132744684c67865c01eca027a8c9946b4c5a19da57c82f1af0373dc83b7 gameinfo.xml
|
|
||||||
ff08aacc52bbdc95616320800da3eaee0c0ba5fafdd962bf04f9655859409764 licenses/CREDITS.md
|
|
||||||
b84fdd2c3da5db56385cdbb639795e90aa3e035c53bc3591135f18df3331451f licenses/Tuffy-LICENSE.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
The original catalog porter credit, `Voyager`, remains in `port.json`.
|
|
||||||
|
|
||||||
## muOS H700 controller mapping
|
|
||||||
|
|
||||||
Godot 4.7.1 and the SDL2 utilities shipped by muOS identify the same virtual
|
|
||||||
controller differently. The PortMaster SDL2 database uses GUID
|
|
||||||
`19000000010000000100000000010000`, while Godot reports GUID
|
|
||||||
`19004ca6010000000100000000010000`. Passing the SDL2 entry unchanged leaves
|
|
||||||
most controls unmapped in Godot.
|
|
||||||
|
|
||||||
The device-verified Godot mapping is:
|
|
||||||
|
|
||||||
- A `b0`, B `b1`, Y `b2`, X `b3`
|
|
||||||
- left bumper `b4`, right bumper `b5`
|
|
||||||
- select `b6`, start `b7`, guide `b8`
|
|
||||||
- left-stick click `b9`, left trigger `b10`, right trigger `b11`, right-stick click `b12`
|
|
||||||
- D-pad `h0`; left and right sticks `a0..a3`
|
|
||||||
|
|
||||||
`scripts/portmaster/NETfishing.sh` must replace the incompatible SDL2 entry
|
|
||||||
with the Godot entry when that SDL2 GUID is selected. Do not append the two
|
|
||||||
entries with a newline: WestonPack evaluates launcher arguments through a
|
|
||||||
shell and treats the second line as a command. Pass the single selected mapping
|
|
||||||
as `SDL_GAMECONTROLLERCONFIG` in the game command after Weston initializes;
|
|
||||||
Weston sources PortMaster's control file internally and otherwise restores the
|
|
||||||
SDL2 value.
|
|
||||||
|
|
||||||
Do not call `Input.add_joy_mapping(..., true)` for a recognized connected muOS
|
|
||||||
controller. Updating this virtual controller after connection can stop Godot
|
|
||||||
from delivering its standardized controller events until restart.
|
|
||||||
|
|
||||||
Before shipping a PortMaster build on H700 hardware, verify all of the
|
|
||||||
following in the installed game:
|
|
||||||
|
|
||||||
- A, B, X, and Y each register independently.
|
|
||||||
- The D-pad navigates every menu direction.
|
|
||||||
- Both sticks, both bumpers, both triggers, Select, Start, L3, and R3 register.
|
|
||||||
- A single button never cancels auto-map.
|
|
||||||
- Holding both bumpers together for 1.25 seconds cancels auto-map.
|
|
||||||
16
main/main.gd
16
main/main.gd
|
|
@ -380,9 +380,6 @@ func _initialize_after_data_root() -> void:
|
||||||
_shop_interaction.local_player_range_changed.connect(
|
_shop_interaction.local_player_range_changed.connect(
|
||||||
_on_shop_range_changed
|
_on_shop_range_changed
|
||||||
)
|
)
|
||||||
_game_ui.set_shop_npc_player_in_range(
|
|
||||||
_shop_interaction.is_local_player_in_range()
|
|
||||||
)
|
|
||||||
_save_manager.setup(
|
_save_manager.setup(
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
_player.collection_log,
|
_player.collection_log,
|
||||||
|
|
@ -1102,9 +1099,6 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
):
|
):
|
||||||
_game_ui.set_shop_prompt_visible(false)
|
_game_ui.set_shop_prompt_visible(false)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
elif event is InputEventJoypadButton and _player != null:
|
|
||||||
_player.hotbar.clear_slot(_player.hotbar.get_selected_slot())
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
|
|
@ -1125,9 +1119,6 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
return
|
return
|
||||||
_apply_world_pixelation(settings.world_pixel_size)
|
_apply_world_pixelation(settings.world_pixel_size)
|
||||||
_ui_pixelation.set_pixel_size(settings.ui_pixel_size)
|
_ui_pixelation.set_pixel_size(settings.ui_pixel_size)
|
||||||
_ui_pixelation.set_on_screen_keyboard_enabled(
|
|
||||||
settings.on_screen_keyboard_enabled
|
|
||||||
)
|
|
||||||
_game_ui.get_title_screen().set_world_pixelation(
|
_game_ui.get_title_screen().set_world_pixelation(
|
||||||
settings.world_pixel_size
|
settings.world_pixel_size
|
||||||
)
|
)
|
||||||
|
|
@ -1150,11 +1141,7 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
func _apply_world_pixelation(pixel_size: int) -> void:
|
func _apply_world_pixelation(pixel_size: int) -> void:
|
||||||
var root_viewport: Viewport = get_viewport()
|
var root_viewport: Viewport = get_viewport()
|
||||||
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
|
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
|
||||||
root_viewport.scaling_3d_scale = (
|
root_viewport.scaling_3d_scale = 1.0
|
||||||
0.75
|
|
||||||
if OS.get_environment("NETFISHING_LOW_END") == "1"
|
|
||||||
else 1.0
|
|
||||||
)
|
|
||||||
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
|
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
|
||||||
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
||||||
root_viewport.use_taa = false
|
root_viewport.use_taa = false
|
||||||
|
|
@ -1683,7 +1670,6 @@ func _exit_tree() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_shop_range_changed(in_range: bool) -> void:
|
func _on_shop_range_changed(in_range: bool) -> void:
|
||||||
_game_ui.set_shop_npc_player_in_range(in_range)
|
|
||||||
if not in_range:
|
if not in_range:
|
||||||
_game_ui.get_fishing_shop().close_for_range_exit()
|
_game_ui.get_fishing_shop().close_for_range_exit()
|
||||||
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
||||||
|
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
readonly TEMPLATE_ROOT="${SCRIPT_DIR}/portmaster"
|
|
||||||
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
|
||||||
|
|
||||||
PACKAGE_ONLY=0
|
|
||||||
if [[ "${1:-}" == "--package-only" ]]; then
|
|
||||||
PACKAGE_ONLY=1
|
|
||||||
shift
|
|
||||||
fi
|
|
||||||
if [[ $# -ne 0 ]]; then
|
|
||||||
echo "Usage: $0 [--package-only]" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
readonly PROJECT_VERSION="$(
|
|
||||||
sed -n 's/^config\/version="\([^"]*\)"/\1/p' "${PROJECT_ROOT}/project.godot"
|
|
||||||
)"
|
|
||||||
if [[ -z "${PROJECT_VERSION}" ]]; then
|
|
||||||
echo "Could not read application/config/version from project.godot." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
readonly RELEASE_TAG="v${PROJECT_VERSION}"
|
|
||||||
readonly RELEASE_ROOT="${PROJECT_ROOT}/builds/${RELEASE_TAG}"
|
|
||||||
readonly ARM64_ROOT="${RELEASE_ROOT}/linux-arm64"
|
|
||||||
readonly ARM64_EXECUTABLE="${ARM64_ROOT}/NETfishing.arm64"
|
|
||||||
readonly ARM64_PCK="${ARM64_ROOT}/NETfishing.pck"
|
|
||||||
readonly STAGE_ROOT="${RELEASE_ROOT}/portmaster-stage"
|
|
||||||
readonly GAME_ROOT="${STAGE_ROOT}/netfishing"
|
|
||||||
readonly ARCHIVE="${RELEASE_ROOT}/netfishing.zip"
|
|
||||||
readonly LEGACY_ARCHIVE="${RELEASE_ROOT}/netfishing-${RELEASE_TAG}-portmaster-arm64.zip"
|
|
||||||
|
|
||||||
readonly HEAD_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
|
||||||
readonly TAG_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse "${RELEASE_TAG}^{commit}")"
|
|
||||||
if [[ "${HEAD_COMMIT}" != "${TAG_COMMIT}" ]]; then
|
|
||||||
echo "HEAD does not match ${RELEASE_TAG}. Refusing to package mutable source." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "${NETFISHING_ALLOW_DIRTY_RELEASE:-0}" != "1" ]]; then
|
|
||||||
dirty_tree=0
|
|
||||||
git -C "${PROJECT_ROOT}" diff --quiet || dirty_tree=1
|
|
||||||
git -C "${PROJECT_ROOT}" diff --cached --quiet || dirty_tree=1
|
|
||||||
if [[ -n "$(git -C "${PROJECT_ROOT}" status --porcelain --untracked-files=normal)" ]]; then
|
|
||||||
dirty_tree=1
|
|
||||||
fi
|
|
||||||
if [[ ${dirty_tree} -ne 0 ]]; then
|
|
||||||
echo "The repository is not clean. Refusing to build a release package." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
for command_name in git file zip unzip sha256sum; do
|
|
||||||
command -v "${command_name}" >/dev/null 2>&1 || {
|
|
||||||
echo "Required command not found: ${command_name}" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
done
|
|
||||||
|
|
||||||
mkdir -p -- "${ARM64_ROOT}" "${RELEASE_ROOT}"
|
|
||||||
if [[ ${PACKAGE_ONLY} -eq 0 ]]; then
|
|
||||||
rm -f -- "${ARM64_EXECUTABLE}" "${ARM64_PCK}"
|
|
||||||
"${GODOT_BIN}" \
|
|
||||||
--headless \
|
|
||||||
--path "${PROJECT_ROOT}" \
|
|
||||||
--export-release "Linux ARM64"
|
|
||||||
fi
|
|
||||||
|
|
||||||
test -s "${ARM64_EXECUTABLE}"
|
|
||||||
test -s "${ARM64_PCK}"
|
|
||||||
file "${ARM64_EXECUTABLE}" | grep -q "ARM aarch64"
|
|
||||||
|
|
||||||
rm -rf -- "${STAGE_ROOT}"
|
|
||||||
rm -f -- "${ARCHIVE}" "${LEGACY_ARCHIVE}"
|
|
||||||
mkdir -p -- "${GAME_ROOT}/licenses"
|
|
||||||
|
|
||||||
install -m 0755 "${TEMPLATE_ROOT}/NETfishing.sh" "${STAGE_ROOT}/NETfishing.sh"
|
|
||||||
install -m 0644 "${TEMPLATE_ROOT}/port.json" "${STAGE_ROOT}/port.json"
|
|
||||||
install -m 0755 "${ARM64_EXECUTABLE}" "${GAME_ROOT}/NETfishing.aarch64"
|
|
||||||
install -m 0644 "${ARM64_PCK}" "${GAME_ROOT}/NETfishing.pck"
|
|
||||||
install -m 0644 "${TEMPLATE_ROOT}/gameinfo.xml" "${GAME_ROOT}/gameinfo.xml"
|
|
||||||
install -m 0644 "${TEMPLATE_ROOT}/screenshot.png" "${GAME_ROOT}/screenshot.png"
|
|
||||||
install -m 0644 \
|
|
||||||
"${TEMPLATE_ROOT}/licenses/CREDITS.md" \
|
|
||||||
"${GAME_ROOT}/licenses/CREDITS.md"
|
|
||||||
install -m 0644 \
|
|
||||||
"${TEMPLATE_ROOT}/licenses/Tuffy-LICENSE.txt" \
|
|
||||||
"${GAME_ROOT}/licenses/Tuffy-LICENSE.txt"
|
|
||||||
|
|
||||||
cat >"${GAME_ROOT}/BUILD-INFO.txt" <<EOF
|
|
||||||
NETfishing PortMaster ARM64 build
|
|
||||||
|
|
||||||
Project version: ${PROJECT_VERSION}
|
|
||||||
Source commit: ${TAG_COMMIT}
|
|
||||||
Engine/export template: $("${GODOT_BIN}" --version)
|
|
||||||
Architecture: AArch64
|
|
||||||
Minimum linked GLIBC symbol version: GLIBC_2.28
|
|
||||||
Rendering method: gl_compatibility
|
|
||||||
|
|
||||||
The executable and PCK were exported from the release commit listed above.
|
|
||||||
Repository working-tree changes were not included in game content.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${GAME_ROOT}/README.md" <<EOF
|
|
||||||
# NETfishing for PortMaster
|
|
||||||
|
|
||||||
This package contains NETfishing \`${RELEASE_TAG}\`, exported from Godot 4.7.1
|
|
||||||
for 64-bit ARM Linux and wrapped for PortMaster.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Install the complete \`netfishing.zip\` archive with PortMaster or HarbourMaster.
|
|
||||||
The package requires an ARM64 device, two analog sticks, GLIBC 2.28 or newer,
|
|
||||||
and the \`weston_pkg_0.2\` runtime.
|
|
||||||
|
|
||||||
Save data and device-local configuration remain under \`netfishing/conf/\`.
|
|
||||||
See \`netfishing/licenses/\` for bundled credits and license information.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
(
|
|
||||||
cd "${STAGE_ROOT}"
|
|
||||||
zip -qry "${ARCHIVE}" NETfishing.sh netfishing port.json
|
|
||||||
)
|
|
||||||
|
|
||||||
unzip -tq "${ARCHIVE}" >/dev/null
|
|
||||||
readonly TOP_LEVELS="$(
|
|
||||||
unzip -Z1 "${ARCHIVE}" | cut -d/ -f1 | sort -u
|
|
||||||
)"
|
|
||||||
readonly EXPECTED_TOP_LEVELS="$(printf '%s\n' NETfishing.sh netfishing port.json)"
|
|
||||||
if [[ "${TOP_LEVELS}" != "${EXPECTED_TOP_LEVELS}" ]]; then
|
|
||||||
echo "Unexpected PortMaster archive roots:" >&2
|
|
||||||
printf '%s\n' "${TOP_LEVELS}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
grep -q '"version": 4' "${STAGE_ROOT}/port.json"
|
|
||||||
grep -q '"name": "netfishing.zip"' "${STAGE_ROOT}/port.json"
|
|
||||||
grep -q '^# PORTMASTER: netfishing.zip, NETfishing.sh$' \
|
|
||||||
"${STAGE_ROOT}/NETfishing.sh"
|
|
||||||
if grep -q 'GPTOKEYB' "${STAGE_ROOT}/NETfishing.sh"; then
|
|
||||||
echo "GPTOKEYB must not be enabled for NETfishing." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if unzip -Z1 "${ARCHIVE}" | grep -E \
|
|
||||||
'(^|/)(conf|\.git|\.godot|logs?|saves?|identities?)(/|$)|\.(gd|tscn|blend)$' \
|
|
||||||
>/dev/null; then
|
|
||||||
echo "The PortMaster archive contains prohibited release content." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sha256sum "${ARCHIVE}"
|
|
||||||
echo "PortMaster package created at ${ARCHIVE}"
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# PORTMASTER: netfishing.zip, NETfishing.sh
|
|
||||||
|
|
||||||
XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
|
|
||||||
|
|
||||||
if [ -d "/opt/system/Tools/PortMaster/" ]; then
|
|
||||||
controlfolder="/opt/system/Tools/PortMaster"
|
|
||||||
elif [ -d "/opt/tools/PortMaster/" ]; then
|
|
||||||
controlfolder="/opt/tools/PortMaster"
|
|
||||||
elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then
|
|
||||||
controlfolder="$XDG_DATA_HOME/PortMaster"
|
|
||||||
else
|
|
||||||
controlfolder="/roms/ports/PortMaster"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$controlfolder/control.txt" ]; then
|
|
||||||
echo "PortMaster control.txt was not found at $controlfolder" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
source "$controlfolder/control.txt"
|
|
||||||
[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
|
|
||||||
get_controls
|
|
||||||
|
|
||||||
GAMEDIR="/${directory}/ports/netfishing"
|
|
||||||
if [ ! -d "$GAMEDIR" ] && [ -d "/mnt/mmc/ports/netfishing" ]; then
|
|
||||||
GAMEDIR="/mnt/mmc/ports/netfishing"
|
|
||||||
fi
|
|
||||||
|
|
||||||
CONFDIR="$GAMEDIR/conf"
|
|
||||||
GAME_EXECUTABLE="$GAMEDIR/NETfishing.aarch64"
|
|
||||||
WESTON_DIR="/tmp/netfishing-weston"
|
|
||||||
WESTON_RUNTIME="weston_pkg_0.2"
|
|
||||||
HARBOURMASTER="$controlfolder/harbourmaster"
|
|
||||||
if [ ! -x "$HARBOURMASTER" ] && [ -x "/mnt/mmc/MUOS/PortMaster/harbourmaster" ]; then
|
|
||||||
HARBOURMASTER="/mnt/mmc/MUOS/PortMaster/harbourmaster"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$CONFDIR/data" "$CONFDIR/config" "$CONFDIR/cache" "$WESTON_DIR"
|
|
||||||
chmod +x "$GAME_EXECUTABLE"
|
|
||||||
|
|
||||||
> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
|
|
||||||
|
|
||||||
if [ ! -f "$controlfolder/libs/${WESTON_RUNTIME}.squashfs" ]; then
|
|
||||||
if [ ! -x "$HARBOURMASTER" ]; then
|
|
||||||
pm_message "NETfishing requires the latest PortMaster and WestonPack runtime."
|
|
||||||
sleep 5
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
$ESUDO "$HARBOURMASTER" --quiet --no-check runtime_check "${WESTON_RUNTIME}.squashfs"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$controlfolder/libs/${WESTON_RUNTIME}.squashfs" ]; then
|
|
||||||
pm_message "WestonPack could not be installed. Check the network connection and PortMaster runtime manager."
|
|
||||||
sleep 5
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$PM_CAN_MOUNT" != "N" ]]; then
|
|
||||||
$ESUDO umount "$WESTON_DIR" 2>/dev/null
|
|
||||||
fi
|
|
||||||
$ESUDO mount "$controlfolder/libs/${WESTON_RUNTIME}.squashfs" "$WESTON_DIR"
|
|
||||||
|
|
||||||
cd "$GAMEDIR" || exit 1
|
|
||||||
|
|
||||||
# Godot 4.7 identifies the muOS H700 virtual controller with a CRC-bearing
|
|
||||||
# GUID and numbers its buttons without the volume keys exposed by SDL2. The
|
|
||||||
# PortMaster SDL2 database entry therefore cannot drive Godot's gamepad layer.
|
|
||||||
GODOT_MUOS_MAPPING="19004ca6010000000100000000010000,muOS-Keys,a:b0,b:b1,x:b3,y:b2,leftshoulder:b4,rightshoulder:b5,lefttrigger:b10,righttrigger:b11,guide:b8,start:b7,back:b6,dpup:h0.1,dpleft:h0.8,dpright:h0.2,dpdown:h0.4,leftx:a0,lefty:a1,leftstick:b9,rightx:a2,righty:a3,rightstick:b12,platform:Linux,"
|
|
||||||
MUOS_SDL2_GUID="19000000010000000100000000010000"
|
|
||||||
if [[ "$sdl_controllerconfig" == "${MUOS_SDL2_GUID},"* ]]; then
|
|
||||||
netfishing_controllerconfig="$GODOT_MUOS_MAPPING"
|
|
||||||
elif [ -n "$sdl_controllerconfig" ]; then
|
|
||||||
netfishing_controllerconfig="$sdl_controllerconfig"
|
|
||||||
else
|
|
||||||
netfishing_controllerconfig="$GODOT_MUOS_MAPPING"
|
|
||||||
fi
|
|
||||||
|
|
||||||
NETFISHING_GODOT_OPTIONS=()
|
|
||||||
NETFISHING_GAME_ENVIRONMENT=()
|
|
||||||
DEVICE_COMPATIBILITY=""
|
|
||||||
if [[ -r /proc/device-tree/compatible ]]; then
|
|
||||||
DEVICE_COMPATIBILITY="$(tr '\0' ' ' </proc/device-tree/compatible)"
|
|
||||||
elif [[ -r /sys/firmware/devicetree/base/compatible ]]; then
|
|
||||||
DEVICE_COMPATIBILITY="$(
|
|
||||||
tr '\0' ' ' </sys/firmware/devicetree/base/compatible
|
|
||||||
)"
|
|
||||||
fi
|
|
||||||
case "$DEVICE_COMPATIBILITY" in
|
|
||||||
*allwinner,h616*|*sun50iw9p1*|*allwinner,sun50i-h700*)
|
|
||||||
NETFISHING_GAME_ENVIRONMENT+=("NETFISHING_LOW_END=1")
|
|
||||||
NETFISHING_GODOT_OPTIONS+=(
|
|
||||||
--single-window
|
|
||||||
--disable-vsync
|
|
||||||
--max-fps 30
|
|
||||||
--audio-output-latency 40
|
|
||||||
)
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
$ESUDO env CRUSTY_RESOLUTION="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \
|
|
||||||
"$WESTON_DIR/westonwrap.sh" headless noop kiosk crusty_x11egl \
|
|
||||||
XDG_DATA_HOME="$CONFDIR/data" \
|
|
||||||
XDG_CONFIG_HOME="$CONFDIR/config" \
|
|
||||||
XDG_CACHE_HOME="$CONFDIR/cache" \
|
|
||||||
SDL_GAMECONTROLLERCONFIG="$netfishing_controllerconfig" \
|
|
||||||
GODOT_SILENCE_ROOT_WARNING=1 \
|
|
||||||
"${NETFISHING_GAME_ENVIRONMENT[@]}" \
|
|
||||||
"$GAME_EXECUTABLE" \
|
|
||||||
"${NETFISHING_GODOT_OPTIONS[@]}" \
|
|
||||||
--resolution "${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \
|
|
||||||
--fullscreen \
|
|
||||||
--rendering-driver opengl3_es \
|
|
||||||
--audio-driver ALSA
|
|
||||||
|
|
||||||
$ESUDO "$WESTON_DIR/westonwrap.sh" cleanup
|
|
||||||
if [[ "$PM_CAN_MOUNT" != "N" ]]; then
|
|
||||||
$ESUDO umount "$WESTON_DIR" 2>/dev/null
|
|
||||||
fi
|
|
||||||
pm_finish
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<gameList>
|
|
||||||
<game>
|
|
||||||
<path>./NETfishing.sh</path>
|
|
||||||
<name>NETfishing</name>
|
|
||||||
<desc>A relaxed multiplayer fishing game about exploring a small island, catching and collecting fish, customizing your character, and spending time by the water.</desc>
|
|
||||||
<releasedate>20260804T000000</releasedate>
|
|
||||||
<developer>NETfishing project</developer>
|
|
||||||
<publisher>Independent</publisher>
|
|
||||||
<genre>adventure, simulation</genre>
|
|
||||||
<image>./netfishing/screenshot.png</image>
|
|
||||||
</game>
|
|
||||||
</gameList>
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
# Credits
|
|
||||||
|
|
||||||
## Project direction
|
|
||||||
|
|
||||||
NETfishing is designed, directed, reviewed, and released by its project
|
|
||||||
creator. A public-facing creator or studio credit has not yet been recorded in
|
|
||||||
this repository.
|
|
||||||
|
|
||||||
## Development assistance
|
|
||||||
|
|
||||||
Development has used automated coding assistance for implementation,
|
|
||||||
investigation, testing, documentation, and review. Design choices, supplied
|
|
||||||
artwork, acceptance decisions, repository operations, and releases remain
|
|
||||||
human-controlled.
|
|
||||||
|
|
||||||
This disclosure is intentionally about process. Commit style, code style, or
|
|
||||||
repository metadata cannot reliably prove whether a particular change was
|
|
||||||
written with or without assistance.
|
|
||||||
|
|
||||||
## Fonts
|
|
||||||
|
|
||||||
- Tuffy: Thatcher Ulrich, Karoly Barta, and Michael Evans. The bundled license
|
|
||||||
dedicates the work to the public domain; see `ui/fonts/Tuffy-LICENSE.txt`.
|
|
||||||
- Seattle Avenue: bundled as `ui/fonts/seattle_avenue.otf`. Author and
|
|
||||||
redistribution terms still need to be recorded before public distribution.
|
|
||||||
|
|
||||||
## Music
|
|
||||||
|
|
||||||
- `audio/music/title/as_in_four_wolves.ogg`: original title/ambient music by
|
|
||||||
the project owner.
|
|
||||||
|
|
||||||
## Sound effects
|
|
||||||
|
|
||||||
- “Spinning reel.wav” by tosha73, used as the source for the fishing fight and
|
|
||||||
manual-reeling loops. Freesound sound 509902, Creative Commons Zero (CC0):
|
|
||||||
https://freesound.org/s/509902/
|
|
||||||
- “Gentle Ocean Waves Loop” by kkenny101, used for the saltwater shoreline
|
|
||||||
ambience. Freesound sound 852826, Creative Commons Zero (CC0):
|
|
||||||
https://freesound.org/s/852826/
|
|
||||||
- “Quick Water Droplet” by qubodup, used for the bobber water-impact sound.
|
|
||||||
Freesound sound 792931, Creative Commons Zero (CC0):
|
|
||||||
https://freesound.org/s/792931/
|
|
||||||
|
|
||||||
## Artwork
|
|
||||||
|
|
||||||
- 2D artwork: Rheannon Eisworth, contributor.
|
|
||||||
- 3D models: original work by the project owner.
|
|
||||||
|
|
||||||
Asset categories and release-record requirements are listed in
|
|
||||||
[`docs/ASSET-PROVENANCE.md`](docs/ASSET-PROVENANCE.md).
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
We, the copyright holders of this work, hereby release it into the
|
|
||||||
public domain. This applies worldwide.
|
|
||||||
|
|
||||||
In case this is not legally possible,
|
|
||||||
|
|
||||||
We grant any entity the right to use this work for any purpose, without
|
|
||||||
any conditions, unless such conditions are required by law.
|
|
||||||
|
|
||||||
Thatcher Ulrich <tu@tulrich.com> http://tulrich.com
|
|
||||||
Karoly Barta bartakarcsi@gmail.com
|
|
||||||
Michael Evans http://www.evertype.com
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
{
|
|
||||||
"version": 4,
|
|
||||||
"name": "netfishing.zip",
|
|
||||||
"items": [
|
|
||||||
"NETfishing.sh",
|
|
||||||
"netfishing/"
|
|
||||||
],
|
|
||||||
"items_opt": null,
|
|
||||||
"attr": {
|
|
||||||
"title": "NETfishing",
|
|
||||||
"porter": [
|
|
||||||
"Voyager",
|
|
||||||
"asellite"
|
|
||||||
],
|
|
||||||
"desc": "A relaxed multiplayer fishing game about exploring a small island, catching and collecting fish, customizing your character, and spending time by the water.",
|
|
||||||
"desc_md": null,
|
|
||||||
"inst": "Ready to run on supported ARM64 PortMaster devices. This alpha package requires two analog sticks.",
|
|
||||||
"inst_md": null,
|
|
||||||
"genres": [
|
|
||||||
"adventure",
|
|
||||||
"simulation"
|
|
||||||
],
|
|
||||||
"image": {},
|
|
||||||
"rtr": false,
|
|
||||||
"exp": false,
|
|
||||||
"runtime": [
|
|
||||||
"weston_pkg_0.2.squashfs"
|
|
||||||
],
|
|
||||||
"store": [],
|
|
||||||
"availability": "full",
|
|
||||||
"reqs": [
|
|
||||||
"analog_2"
|
|
||||||
],
|
|
||||||
"arch": [
|
|
||||||
"aarch64"
|
|
||||||
],
|
|
||||||
"min_glibc": "2.28"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 153 KiB |
|
|
@ -1,40 +0,0 @@
|
||||||
[remap]
|
|
||||||
|
|
||||||
importer="texture"
|
|
||||||
type="CompressedTexture2D"
|
|
||||||
uid="uid://ww0w8iubiwsf"
|
|
||||||
path="res://.godot/imported/screenshot.png-6eed5589b80ced9ab24aed11cadf34f5.ctex"
|
|
||||||
metadata={
|
|
||||||
"vram_texture": false
|
|
||||||
}
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://scripts/portmaster/screenshot.png"
|
|
||||||
dest_files=["res://.godot/imported/screenshot.png-6eed5589b80ced9ab24aed11cadf34f5.ctex"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
compress/mode=0
|
|
||||||
compress/high_quality=false
|
|
||||||
compress/lossy_quality=0.7
|
|
||||||
compress/uastc_level=0
|
|
||||||
compress/rdo_quality_loss=0.0
|
|
||||||
compress/hdr_compression=1
|
|
||||||
compress/normal_map=0
|
|
||||||
compress/channel_pack=0
|
|
||||||
mipmaps/generate=false
|
|
||||||
mipmaps/limit=-1
|
|
||||||
roughness/mode=0
|
|
||||||
roughness/src_normal=""
|
|
||||||
process/channel_remap/red=0
|
|
||||||
process/channel_remap/green=1
|
|
||||||
process/channel_remap/blue=2
|
|
||||||
process/channel_remap/alpha=3
|
|
||||||
process/fix_alpha_border=true
|
|
||||||
process/premult_alpha=false
|
|
||||||
process/normal_map_invert_y=false
|
|
||||||
process/hdr_as_srgb=false
|
|
||||||
process/hdr_clamp_exposure=false
|
|
||||||
process/size_limit=0
|
|
||||||
detect_3d/compress_to=1
|
|
||||||
|
|
@ -13,19 +13,19 @@ const MAX_PROFILE_BYTES: int = 1024 * 1024
|
||||||
const CAPTURE_AXIS_THRESHOLD: float = 0.55
|
const CAPTURE_AXIS_THRESHOLD: float = 0.55
|
||||||
const CAPTURE_AXIS_RELEASE_THRESHOLD: float = 0.30
|
const CAPTURE_AXIS_RELEASE_THRESHOLD: float = 0.30
|
||||||
const ACTIVE_DEVICE_AXIS_THRESHOLD: float = 0.35
|
const ACTIVE_DEVICE_AXIS_THRESHOLD: float = 0.35
|
||||||
const MUOS_MAPPING_REVISION: String = "muos-v4"
|
const MUOS_MAPPING_REVISION: String = "muos-v3"
|
||||||
const MUOS_CONTROLLER_NAMES: Array[String] = [
|
const MUOS_CONTROLLER_NAMES: Array[String] = [
|
||||||
"muOS-Keys",
|
"muOS-Keys",
|
||||||
"Deeplay-keys",
|
"Deeplay-keys",
|
||||||
]
|
]
|
||||||
const MUOS_MAPPING_BINDINGS: String = (
|
const MUOS_MAPPING_BINDINGS: String = (
|
||||||
"a:b0,b:b1,x:b3,y:b2,"
|
"a:b4,b:b3,x:b5,y:b6,"
|
||||||
+ "leftshoulder:b4,rightshoulder:b5,"
|
+ "leftshoulder:b7,rightshoulder:b8,"
|
||||||
+ "lefttrigger:b10,righttrigger:b11,"
|
+ "lefttrigger:b13,righttrigger:b14,"
|
||||||
+ "guide:b8,start:b7,back:b6,"
|
+ "guide:b11,start:b10,back:b9,"
|
||||||
+ "dpup:h0.1,dpleft:h0.8,dpright:h0.2,dpdown:h0.4,"
|
+ "dpup:h0.1,dpleft:h0.8,dpright:h0.2,dpdown:h0.4,"
|
||||||
+ "leftx:a0,lefty:a1,leftstick:b9,"
|
+ "leftx:a0,lefty:a1,leftstick:b12,"
|
||||||
+ "rightx:a2,righty:a3,rightstick:b12,platform:Linux,"
|
+ "rightx:a2,righty:a3,rightstick:b15,platform:Linux,"
|
||||||
)
|
)
|
||||||
const ROLE_A: StringName = &"a"
|
const ROLE_A: StringName = &"a"
|
||||||
const ROLE_B: StringName = &"b"
|
const ROLE_B: StringName = &"b"
|
||||||
|
|
@ -739,8 +739,8 @@ static func should_install_muos_compatibility_mapping(
|
||||||
controller_name: String,
|
controller_name: String,
|
||||||
is_known: bool,
|
is_known: bool,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
# Never update an already-recognized virtual controller at runtime. Godot
|
# Current SDL databases should remain authoritative. This fallback exists
|
||||||
# can stop delivering its standardized input events after that replacement.
|
# only for legacy muOS virtual controllers that SDL cannot identify.
|
||||||
return is_muos_controller_name(controller_name) and not is_known
|
return is_muos_controller_name(controller_name) and not is_known
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192]
|
||||||
@export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005
|
@export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005
|
||||||
@export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
|
@export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
|
||||||
@export var invert_camera_y: bool = false
|
@export var invert_camera_y: bool = false
|
||||||
@export var on_screen_keyboard_enabled: bool = false
|
|
||||||
@export var chat_draft: String = ""
|
@export var chat_draft: String = ""
|
||||||
@export var chat_collapsed: bool = false
|
@export var chat_collapsed: bool = false
|
||||||
@export var chat_dock_right: bool = false
|
@export var chat_dock_right: bool = false
|
||||||
|
|
@ -64,7 +63,6 @@ func copy() -> PlayerSettings:
|
||||||
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
||||||
result.controller_camera_sensitivity = controller_camera_sensitivity
|
result.controller_camera_sensitivity = controller_camera_sensitivity
|
||||||
result.invert_camera_y = invert_camera_y
|
result.invert_camera_y = invert_camera_y
|
||||||
result.on_screen_keyboard_enabled = on_screen_keyboard_enabled
|
|
||||||
result.chat_draft = chat_draft
|
result.chat_draft = chat_draft
|
||||||
result.chat_collapsed = chat_collapsed
|
result.chat_collapsed = chat_collapsed
|
||||||
result.chat_dock_right = chat_dock_right
|
result.chat_dock_right = chat_dock_right
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,6 @@ func load_settings() -> bool:
|
||||||
if (
|
if (
|
||||||
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
|
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
|
||||||
or typeof(camera.get("invert_vertical")) != TYPE_BOOL
|
or typeof(camera.get("invert_vertical")) != TYPE_BOOL
|
||||||
or (
|
|
||||||
accessibility.has("on_screen_keyboard_enabled")
|
|
||||||
and typeof(accessibility["on_screen_keyboard_enabled"]) != TYPE_BOOL
|
|
||||||
)
|
|
||||||
or (
|
or (
|
||||||
accessibility.has("use_readable_interface_font")
|
accessibility.has("use_readable_interface_font")
|
||||||
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
||||||
|
|
@ -92,9 +88,6 @@ func load_settings() -> bool:
|
||||||
-1.0
|
-1.0
|
||||||
)
|
)
|
||||||
loaded.invert_camera_y = camera["invert_vertical"]
|
loaded.invert_camera_y = camera["invert_vertical"]
|
||||||
loaded.on_screen_keyboard_enabled = bool(
|
|
||||||
accessibility.get("on_screen_keyboard_enabled", false)
|
|
||||||
)
|
|
||||||
loaded.world_pixel_size = _read_clamped_integer(
|
loaded.world_pixel_size = _read_clamped_integer(
|
||||||
presentation.get(
|
presentation.get(
|
||||||
"world_pixel_size",
|
"world_pixel_size",
|
||||||
|
|
@ -183,9 +176,6 @@ func save_now() -> bool:
|
||||||
"auto_click_enabled": current_settings.auto_click_enabled,
|
"auto_click_enabled": current_settings.auto_click_enabled,
|
||||||
"auto_click_interval": current_settings.auto_click_interval,
|
"auto_click_interval": current_settings.auto_click_interval,
|
||||||
"use_readable_interface_font": true,
|
"use_readable_interface_font": true,
|
||||||
"on_screen_keyboard_enabled": (
|
|
||||||
current_settings.on_screen_keyboard_enabled
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"camera": {
|
"camera": {
|
||||||
"mouse_sensitivity": current_settings.mouse_camera_sensitivity,
|
"mouse_sensitivity": current_settings.mouse_camera_sensitivity,
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
extends SceneTree
|
|
||||||
|
|
||||||
const FocusPresentationType = preload(
|
|
||||||
"res://ui/controller_focus_presentation.gd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
|
||||||
_run.call_deferred()
|
|
||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
|
||||||
var stage := Control.new()
|
|
||||||
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
root.add_child(stage)
|
|
||||||
var presentation := FocusPresentationType.new()
|
|
||||||
stage.add_child(presentation)
|
|
||||||
var standard_button := Button.new()
|
|
||||||
standard_button.text = "standard"
|
|
||||||
standard_button.custom_minimum_size = Vector2(120.0, 60.0)
|
|
||||||
stage.add_child(standard_button)
|
|
||||||
var authored_selector := Button.new()
|
|
||||||
authored_selector.text = "authored selector"
|
|
||||||
authored_selector.position = Vector2(140.0, 0.0)
|
|
||||||
authored_selector.custom_minimum_size = Vector2(160.0, 60.0)
|
|
||||||
authored_selector.set_meta(&"controller_focus_inversion_disabled", true)
|
|
||||||
stage.add_child(authored_selector)
|
|
||||||
await process_frame
|
|
||||||
|
|
||||||
var controller_event := InputEventJoypadButton.new()
|
|
||||||
controller_event.button_index = JOY_BUTTON_A
|
|
||||||
controller_event.pressed = true
|
|
||||||
presentation._input(controller_event)
|
|
||||||
standard_button.grab_focus()
|
|
||||||
await process_frame
|
|
||||||
_expect(
|
|
||||||
standard_button.material != null,
|
|
||||||
"ordinary controller focus receives inversion",
|
|
||||||
)
|
|
||||||
|
|
||||||
authored_selector.grab_focus()
|
|
||||||
await process_frame
|
|
||||||
_expect(
|
|
||||||
standard_button.material == null,
|
|
||||||
"inversion clears when focus moves",
|
|
||||||
)
|
|
||||||
_expect(
|
|
||||||
authored_selector.material == null,
|
|
||||||
"authored selector backgrounds opt out of inversion",
|
|
||||||
)
|
|
||||||
|
|
||||||
standard_button.grab_focus()
|
|
||||||
await process_frame
|
|
||||||
standard_button.focus_mode = Control.FOCUS_NONE
|
|
||||||
await process_frame
|
|
||||||
_expect(
|
|
||||||
standard_button.material == null,
|
|
||||||
"inversion clears when a focused control is defocused",
|
|
||||||
)
|
|
||||||
|
|
||||||
stage.queue_free()
|
|
||||||
if _failures.is_empty():
|
|
||||||
print("Controller focus presentation validation: PASS")
|
|
||||||
quit(0)
|
|
||||||
return
|
|
||||||
for failure: String in _failures:
|
|
||||||
push_error(failure)
|
|
||||||
quit(1)
|
|
||||||
|
|
||||||
|
|
||||||
func _expect(condition: bool, message: String) -> void:
|
|
||||||
if not condition:
|
|
||||||
_failures.append(message)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://bjay8ki3albk6
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
extends SceneTree
|
|
||||||
|
|
||||||
const ControllerFocusRecoveryType = preload(
|
|
||||||
"res://ui/controller_focus_recovery.gd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _failures: Array[String] = []
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
|
||||||
_run.call_deferred()
|
|
||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
|
||||||
var stage := Control.new()
|
|
||||||
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
root.add_child(stage)
|
|
||||||
var recovery := ControllerFocusRecoveryType.new()
|
|
||||||
stage.add_child(recovery)
|
|
||||||
var option_list := VBoxContainer.new()
|
|
||||||
option_list.position = Vector2(100.0, 100.0)
|
|
||||||
stage.add_child(option_list)
|
|
||||||
var original := _make_button("cute", "cute (show variants)")
|
|
||||||
option_list.add_child(original)
|
|
||||||
await process_frame
|
|
||||||
|
|
||||||
var controller_event := InputEventJoypadButton.new()
|
|
||||||
controller_event.button_index = JOY_BUTTON_A
|
|
||||||
controller_event.pressed = true
|
|
||||||
recovery._input(controller_event)
|
|
||||||
original.grab_focus()
|
|
||||||
await process_frame
|
|
||||||
_expect(root.gui_get_focus_owner() == original, "original option receives focus")
|
|
||||||
|
|
||||||
option_list.remove_child(original)
|
|
||||||
original.queue_free()
|
|
||||||
var replacement := _make_button("cute", "cute (hide variants)")
|
|
||||||
option_list.add_child(replacement)
|
|
||||||
await process_frame
|
|
||||||
await process_frame
|
|
||||||
_expect(
|
|
||||||
root.gui_get_focus_owner() == replacement,
|
|
||||||
"focus follows a rebuilt semantic option",
|
|
||||||
)
|
|
||||||
|
|
||||||
var explicit_target := _make_button("explicit", "explicit")
|
|
||||||
option_list.add_child(explicit_target)
|
|
||||||
option_list.remove_child(replacement)
|
|
||||||
replacement.queue_free()
|
|
||||||
explicit_target.grab_focus()
|
|
||||||
await process_frame
|
|
||||||
_expect(
|
|
||||||
root.gui_get_focus_owner() == explicit_target,
|
|
||||||
"explicit focus changes take precedence over recovery",
|
|
||||||
)
|
|
||||||
var leave_world_ui_event := InputEventJoypadButton.new()
|
|
||||||
leave_world_ui_event.button_index = JOY_BUTTON_LEFT_SHOULDER
|
|
||||||
leave_world_ui_event.pressed = true
|
|
||||||
recovery._input(leave_world_ui_event)
|
|
||||||
root.gui_release_focus()
|
|
||||||
await process_frame
|
|
||||||
await process_frame
|
|
||||||
_expect(
|
|
||||||
root.gui_get_focus_owner() == null,
|
|
||||||
"LB intentionally leaving world UI is never recovered",
|
|
||||||
)
|
|
||||||
|
|
||||||
stage.queue_free()
|
|
||||||
if _failures.is_empty():
|
|
||||||
print("Controller focus recovery validation: PASS")
|
|
||||||
quit(0)
|
|
||||||
return
|
|
||||||
for failure: String in _failures:
|
|
||||||
push_error(failure)
|
|
||||||
quit(1)
|
|
||||||
|
|
||||||
|
|
||||||
func _make_button(text: String, tooltip: String) -> Button:
|
|
||||||
var button := Button.new()
|
|
||||||
button.text = text
|
|
||||||
button.tooltip_text = tooltip
|
|
||||||
button.custom_minimum_size = Vector2(120.0, 48.0)
|
|
||||||
return button
|
|
||||||
|
|
||||||
|
|
||||||
func _expect(condition: bool, message: String) -> void:
|
|
||||||
if not condition:
|
|
||||||
_failures.append(message)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://cqk75fxykqrhs
|
|
||||||
|
|
@ -18,8 +18,6 @@ func _run() -> void:
|
||||||
root.add_child(manager)
|
root.add_child(manager)
|
||||||
await process_frame
|
await process_frame
|
||||||
var failure: String = _validate_manager(manager)
|
var failure: String = _validate_manager(manager)
|
||||||
if failure.is_empty():
|
|
||||||
failure = _validate_portmaster_launcher()
|
|
||||||
if failure.is_empty():
|
if failure.is_empty():
|
||||||
failure = _validate_auto_map(manager)
|
failure = _validate_auto_map(manager)
|
||||||
if failure.is_empty():
|
if failure.is_empty():
|
||||||
|
|
@ -42,19 +40,19 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for expected_binding: String in [
|
for expected_binding: String in [
|
||||||
"a:b0",
|
"a:b4",
|
||||||
"b:b1",
|
"b:b3",
|
||||||
"x:b3",
|
"x:b5",
|
||||||
"y:b2",
|
"y:b6",
|
||||||
"leftshoulder:b4",
|
"leftshoulder:b7",
|
||||||
"rightshoulder:b5",
|
"rightshoulder:b8",
|
||||||
"lefttrigger:b10",
|
"lefttrigger:b13",
|
||||||
"righttrigger:b11",
|
"righttrigger:b14",
|
||||||
"back:b6",
|
"back:b9",
|
||||||
"start:b7",
|
"start:b10",
|
||||||
"guide:b8",
|
"guide:b11",
|
||||||
"leftstick:b9",
|
"leftstick:b12",
|
||||||
"rightstick:b12",
|
"rightstick:b15",
|
||||||
"leftx:a0",
|
"leftx:a0",
|
||||||
"righty:a3",
|
"righty:a3",
|
||||||
]:
|
]:
|
||||||
|
|
@ -81,7 +79,7 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
||||||
"muOS-Keys",
|
"muOS-Keys",
|
||||||
true,
|
true,
|
||||||
):
|
):
|
||||||
return "recognized muOS controller would be replaced at runtime"
|
return "recognized muOS controller would override its SDL mapping"
|
||||||
if ControllerMappingManagerType.should_install_muos_compatibility_mapping(
|
if ControllerMappingManagerType.should_install_muos_compatibility_mapping(
|
||||||
"ordinary controller",
|
"ordinary controller",
|
||||||
false,
|
false,
|
||||||
|
|
@ -196,30 +194,6 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
func _validate_portmaster_launcher() -> String:
|
|
||||||
const launcher_path: String = "res://scripts/portmaster/NETfishing.sh"
|
|
||||||
if not FileAccess.file_exists(launcher_path):
|
|
||||||
return "PortMaster launcher template is missing"
|
|
||||||
var launcher: String = FileAccess.get_file_as_string(launcher_path)
|
|
||||||
for expected_fragment: String in [
|
|
||||||
"19004ca6010000000100000000010000",
|
|
||||||
"19000000010000000100000000010000",
|
|
||||||
"a:b0,b:b1,x:b3,y:b2",
|
|
||||||
"leftshoulder:b4,rightshoulder:b5",
|
|
||||||
"lefttrigger:b10,righttrigger:b11",
|
|
||||||
"guide:b8,start:b7,back:b6",
|
|
||||||
"leftstick:b9",
|
|
||||||
"rightstick:b12",
|
|
||||||
"netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"",
|
|
||||||
"SDL_GAMECONTROLLERCONFIG=\"$netfishing_controllerconfig\" \\",
|
|
||||||
]:
|
|
||||||
if expected_fragment not in launcher:
|
|
||||||
return "PortMaster launcher omitted " + expected_fragment
|
|
||||||
if "$'\\n'" in launcher:
|
|
||||||
return "PortMaster launcher appends mappings across a Weston-unsafe newline"
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
||||||
var panel := ControllerMappingPanelType.new()
|
var panel := ControllerMappingPanelType.new()
|
||||||
root.add_child(panel)
|
root.add_child(panel)
|
||||||
|
|
@ -284,35 +258,13 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
||||||
return "manual remapping still dictates a specific physical input"
|
return "manual remapping still dictates a specific physical input"
|
||||||
if "any button" not in panel._progress_label.text.to_lower():
|
if "any button" not in panel._progress_label.text.to_lower():
|
||||||
return "manual remapping does not request a generic controller input"
|
return "manual remapping does not request a generic controller input"
|
||||||
panel._cancel_capture()
|
var cancel_button := InputEventJoypadButton.new()
|
||||||
panel._begin_auto_map()
|
cancel_button.device = manager.get_active_device_id()
|
||||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
cancel_button.button_index = JOY_BUTTON_B
|
||||||
var lone_button := InputEventJoypadButton.new()
|
cancel_button.pressed = true
|
||||||
lone_button.device = manager.get_active_device_id()
|
manager.controller_input_observed.emit(cancel_button)
|
||||||
lone_button.button_index = JOY_BUTTON_B
|
if panel.is_capturing():
|
||||||
lone_button.pressed = true
|
return "mapped controller back input did not cancel capture"
|
||||||
manager.controller_input_observed.emit(lone_button)
|
|
||||||
if not panel._auto_map_active or panel._auto_map_index != 1:
|
|
||||||
return "a single controller button cancelled auto-map"
|
|
||||||
panel._cancel_capture()
|
|
||||||
panel._begin_auto_map()
|
|
||||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
|
||||||
var left_bumper := InputEventJoypadButton.new()
|
|
||||||
left_bumper.device = manager.get_active_device_id()
|
|
||||||
left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER
|
|
||||||
left_bumper.pressed = true
|
|
||||||
manager.controller_input_observed.emit(left_bumper)
|
|
||||||
var right_bumper := InputEventJoypadButton.new()
|
|
||||||
right_bumper.device = manager.get_active_device_id()
|
|
||||||
right_bumper.button_index = JOY_BUTTON_RIGHT_SHOULDER
|
|
||||||
right_bumper.pressed = true
|
|
||||||
manager.controller_input_observed.emit(right_bumper)
|
|
||||||
panel._process(ControllerMappingPanelType.CANCEL_COMBO_HOLD_SECONDS - 0.01)
|
|
||||||
if not panel._auto_map_active:
|
|
||||||
return "bumper combo cancelled auto-map before the hold threshold"
|
|
||||||
panel._process(0.02)
|
|
||||||
if panel._auto_map_active or panel.is_capturing():
|
|
||||||
return "sustained bumper combo did not cancel auto-map"
|
|
||||||
panel._begin_auto_map()
|
panel._begin_auto_map()
|
||||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||||
var held_left := InputEventJoypadMotion.new()
|
var held_left := InputEventJoypadMotion.new()
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
extends SceneTree
|
|
||||||
|
|
||||||
const ControllerFocusNavigationType = preload(
|
|
||||||
"res://ui/controller_focus_navigation.gd"
|
|
||||||
)
|
|
||||||
const UIReferencePresentationType = preload(
|
|
||||||
"res://ui/ui_reference_presentation.gd"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
|
||||||
call_deferred("_run")
|
|
||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
|
||||||
_validate_spatial_navigation()
|
|
||||||
_validate_controller_hierarchy_contract()
|
|
||||||
_validate_four_by_three_centering()
|
|
||||||
_validate_low_end_profile_contract()
|
|
||||||
print("Controller UI navigation validation: PASS")
|
|
||||||
quit()
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_spatial_navigation() -> void:
|
|
||||||
var host := Control.new()
|
|
||||||
root.add_child(host)
|
|
||||||
var center := _make_button(host, "center", Vector2(200.0, 200.0))
|
|
||||||
var left := _make_button(host, "left", Vector2(50.0, 200.0))
|
|
||||||
var right := _make_button(host, "right", Vector2(350.0, 200.0))
|
|
||||||
var top := _make_button(host, "top", Vector2(200.0, 50.0))
|
|
||||||
var bottom := _make_button(host, "bottom", Vector2(200.0, 350.0))
|
|
||||||
var controls: Array[Control] = [center, left, right, top, bottom]
|
|
||||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
|
||||||
assert(center.get_node(center.focus_neighbor_left) == left)
|
|
||||||
assert(center.get_node(center.focus_neighbor_right) == right)
|
|
||||||
assert(center.get_node(center.focus_neighbor_top) == top)
|
|
||||||
assert(center.get_node(center.focus_neighbor_bottom) == bottom)
|
|
||||||
host.queue_free()
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_controller_hierarchy_contract() -> void:
|
|
||||||
var source: String = FileAccess.get_file_as_string(
|
|
||||||
"res://ui/player_menu.gd"
|
|
||||||
)
|
|
||||||
assert(source.contains("ROLE_POINTER_MODIFIER"))
|
|
||||||
assert(source.contains("ROLE_CAMERA_ZOOM"))
|
|
||||||
assert(source.contains("_handle_controller_secondary_switch"))
|
|
||||||
assert(source.contains("ROLE_LB"))
|
|
||||||
assert(source.contains("ROLE_RB"))
|
|
||||||
assert(source.contains("_reserve_main_navigation_for_page_switching"))
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_four_by_three_centering() -> void:
|
|
||||||
var stage_position: Vector2 = (
|
|
||||||
UIReferencePresentationType.get_stage_position(Vector2(640.0, 480.0))
|
|
||||||
)
|
|
||||||
assert(stage_position.is_equal_approx(Vector2(0.0, 120.0)))
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_low_end_profile_contract() -> void:
|
|
||||||
var launcher: String = FileAccess.get_file_as_string(
|
|
||||||
"res://scripts/portmaster/NETfishing.sh"
|
|
||||||
)
|
|
||||||
assert(launcher.contains("allwinner,h616"))
|
|
||||||
assert(launcher.contains("sun50iw9p1"))
|
|
||||||
assert(launcher.contains("NETFISHING_LOW_END=1"))
|
|
||||||
assert(launcher.contains("--max-fps 30"))
|
|
||||||
assert(launcher.contains("--audio-output-latency 40"))
|
|
||||||
|
|
||||||
|
|
||||||
func _make_button(
|
|
||||||
host: Control,
|
|
||||||
button_name: String,
|
|
||||||
button_position: Vector2,
|
|
||||||
) -> Button:
|
|
||||||
var button := Button.new()
|
|
||||||
button.name = button_name
|
|
||||||
button.position = button_position
|
|
||||||
button.size = Vector2(80.0, 80.0)
|
|
||||||
button.focus_mode = Control.FOCUS_ALL
|
|
||||||
host.add_child(button)
|
|
||||||
return button
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://cbv0ia42rqu41
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
extends SceneTree
|
|
||||||
|
|
||||||
const KeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
|
||||||
const SettingsManagerType = preload(
|
|
||||||
"res://settings/player_settings_manager.gd"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
|
||||||
call_deferred("_run")
|
|
||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
|
||||||
_validate_default_and_persistence()
|
|
||||||
await _validate_keyboard_entry()
|
|
||||||
print("On-screen keyboard validation: PASS")
|
|
||||||
quit()
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_default_and_persistence() -> void:
|
|
||||||
var defaults := PlayerSettings.new()
|
|
||||||
assert(not defaults.on_screen_keyboard_enabled)
|
|
||||||
var manager := SettingsManagerType.new()
|
|
||||||
root.add_child(manager)
|
|
||||||
assert(manager.load_settings())
|
|
||||||
var edited: PlayerSettings = manager.current_settings.copy()
|
|
||||||
edited.on_screen_keyboard_enabled = true
|
|
||||||
assert(manager.apply_settings(edited))
|
|
||||||
var reloaded := SettingsManagerType.new()
|
|
||||||
root.add_child(reloaded)
|
|
||||||
assert(reloaded.load_settings())
|
|
||||||
assert(reloaded.current_settings.on_screen_keyboard_enabled)
|
|
||||||
manager.queue_free()
|
|
||||||
reloaded.queue_free()
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_keyboard_entry() -> void:
|
|
||||||
var host := Control.new()
|
|
||||||
root.add_child(host)
|
|
||||||
var edit := LineEdit.new()
|
|
||||||
edit.focus_mode = Control.FOCUS_ALL
|
|
||||||
host.add_child(edit)
|
|
||||||
var keyboard := KeyboardType.new()
|
|
||||||
host.add_child(keyboard)
|
|
||||||
await process_frame
|
|
||||||
keyboard.set_enabled(true)
|
|
||||||
edit.grab_focus()
|
|
||||||
var activate_event := InputEventJoypadButton.new()
|
|
||||||
activate_event.button_index = JOY_BUTTON_A
|
|
||||||
activate_event.pressed = true
|
|
||||||
keyboard.call("_input", activate_event)
|
|
||||||
assert(keyboard.is_open())
|
|
||||||
keyboard.call("_type_character", "a")
|
|
||||||
keyboard.call("_type_space")
|
|
||||||
keyboard.call("_set_page", KeyboardType.Page.UPPER)
|
|
||||||
keyboard.call("_type_character", "B")
|
|
||||||
assert(edit.text == "a B")
|
|
||||||
keyboard.call("_move_caret", -1)
|
|
||||||
keyboard.call("_type_character", "C")
|
|
||||||
assert(edit.text == "a CB")
|
|
||||||
var backspace_event := InputEventJoypadButton.new()
|
|
||||||
backspace_event.button_index = JOY_BUTTON_X
|
|
||||||
backspace_event.pressed = true
|
|
||||||
keyboard.call("_input", backspace_event)
|
|
||||||
assert(edit.text == "a B")
|
|
||||||
var defocus_event := InputEventJoypadButton.new()
|
|
||||||
defocus_event.button_index = JOY_BUTTON_LEFT_SHOULDER
|
|
||||||
defocus_event.pressed = true
|
|
||||||
keyboard.call("_input", defocus_event)
|
|
||||||
assert(not keyboard.is_open())
|
|
||||||
assert(root.gui_get_focus_owner() == null)
|
|
||||||
edit.grab_focus()
|
|
||||||
keyboard.call("_input", activate_event)
|
|
||||||
assert(keyboard.is_open())
|
|
||||||
var submitted: Array[String] = []
|
|
||||||
edit.text_submitted.connect(func(value: String) -> void:
|
|
||||||
submitted.append(value)
|
|
||||||
)
|
|
||||||
keyboard.call("_submit")
|
|
||||||
assert(not keyboard.is_open())
|
|
||||||
assert(submitted == ["a B"])
|
|
||||||
host.queue_free()
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://bgmt6joqdk37q
|
|
||||||
|
|
@ -94,10 +94,7 @@ func _run() -> void:
|
||||||
assert(player_menu.size.is_equal_approx(
|
assert(player_menu.size.is_equal_approx(
|
||||||
UIReferencePresentationType.REFERENCE_SIZE
|
UIReferencePresentationType.REFERENCE_SIZE
|
||||||
))
|
))
|
||||||
assert(hotbar.position.is_equal_approx(Vector2(
|
assert(hotbar.position.is_equal_approx(Vector2.ZERO))
|
||||||
0.0,
|
|
||||||
canonical_stage.position.y,
|
|
||||||
)))
|
|
||||||
assert(hotbar.size.is_equal_approx(
|
assert(hotbar.size.is_equal_approx(
|
||||||
UIReferencePresentationType.REFERENCE_SIZE
|
UIReferencePresentationType.REFERENCE_SIZE
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -568,21 +568,6 @@ func _refresh_input_ownership() -> void:
|
||||||
)
|
)
|
||||||
_panel.mouse_filter = filter
|
_panel.mouse_filter = filter
|
||||||
_history.mouse_filter = filter
|
_history.mouse_filter = filter
|
||||||
_entry.focus_mode = (
|
|
||||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
|
||||||
)
|
|
||||||
_collapse_button.focus_mode = (
|
|
||||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
|
||||||
)
|
|
||||||
_height_button.focus_mode = (
|
|
||||||
Control.FOCUS_ALL
|
|
||||||
if _opened and not _mobile_mode
|
|
||||||
else Control.FOCUS_NONE
|
|
||||||
)
|
|
||||||
if not _opened:
|
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
if focus_owner in [_entry, _collapse_button, _height_button]:
|
|
||||||
get_viewport().gui_release_focus()
|
|
||||||
|
|
||||||
|
|
||||||
func _clock_panel_style() -> StyleBoxFlat:
|
func _clock_panel_style() -> StyleBoxFlat:
|
||||||
|
|
@ -901,14 +886,9 @@ func _refresh_visibility() -> void:
|
||||||
_collapse_button.show()
|
_collapse_button.show()
|
||||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||||
_panel.show()
|
_panel.show()
|
||||||
_collapse_button.focus_mode = (
|
|
||||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
|
||||||
)
|
|
||||||
_height_button.visible = not _mobile_mode
|
_height_button.visible = not _mobile_mode
|
||||||
_height_button.focus_mode = (
|
_height_button.focus_mode = (
|
||||||
Control.FOCUS_ALL
|
Control.FOCUS_NONE if _mobile_mode else Control.FOCUS_ALL
|
||||||
if _opened and not _mobile_mode
|
|
||||||
else Control.FOCUS_NONE
|
|
||||||
)
|
)
|
||||||
_unread_indicator.visible = collapsed and _collapsed_has_unread
|
_unread_indicator.visible = collapsed and _collapsed_has_unread
|
||||||
_hint.visible = not _opened
|
_hint.visible = not _opened
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,6 @@ var _base_position: Vector2 = Vector2.ZERO
|
||||||
var _presented_size: Vector2 = Vector2.ZERO
|
var _presented_size: Vector2 = Vector2.ZERO
|
||||||
var _compact: bool = false
|
var _compact: bool = false
|
||||||
var _presentation_initialized: bool = false
|
var _presentation_initialized: bool = false
|
||||||
var _controller_preview_active: bool = false
|
|
||||||
var _controller_preview_texture: Texture2D
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -148,15 +146,6 @@ func set_drag_enabled(enabled: bool) -> void:
|
||||||
_hovered = false
|
_hovered = false
|
||||||
|
|
||||||
|
|
||||||
func set_controller_placement_preview(
|
|
||||||
active: bool,
|
|
||||||
texture: Texture2D,
|
|
||||||
) -> void:
|
|
||||||
_controller_preview_active = active
|
|
||||||
_controller_preview_texture = texture
|
|
||||||
refresh()
|
|
||||||
|
|
||||||
|
|
||||||
func refresh() -> void:
|
func refresh() -> void:
|
||||||
if _hotbar == null:
|
if _hotbar == null:
|
||||||
return
|
return
|
||||||
|
|
@ -172,16 +161,11 @@ func refresh() -> void:
|
||||||
if _fish_inventory != null and not catch_id.is_empty()
|
if _fish_inventory != null and not catch_id.is_empty()
|
||||||
else null
|
else null
|
||||||
)
|
)
|
||||||
var assigned_texture: Texture2D = (
|
_item_icon.texture = (
|
||||||
fish_catch.fish.display_texture
|
fish_catch.fish.display_texture
|
||||||
if fish_catch != null
|
if fish_catch != null
|
||||||
else item.icon if item != null else null
|
else item.icon if item != null else null
|
||||||
)
|
)
|
||||||
_item_icon.texture = (
|
|
||||||
_controller_preview_texture
|
|
||||||
if _controller_preview_active
|
|
||||||
else assigned_texture
|
|
||||||
)
|
|
||||||
var quantity: int = (
|
var quantity: int = (
|
||||||
_bag.get_quantity(item_id)
|
_bag.get_quantity(item_id)
|
||||||
if _bag != null and not item_id.is_empty()
|
if _bag != null and not item_id.is_empty()
|
||||||
|
|
@ -193,9 +177,7 @@ func refresh() -> void:
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
_quantity_label.text = quantity_text
|
_quantity_label.text = quantity_text
|
||||||
_quantity_label.visible = (
|
_quantity_label.visible = not quantity_text.is_empty()
|
||||||
not _controller_preview_active and not quantity_text.is_empty()
|
|
||||||
)
|
|
||||||
tooltip_text = (
|
tooltip_text = (
|
||||||
"%s · %.1f lb" % [
|
"%s · %.1f lb" % [
|
||||||
FishQualityType.qualified_name(
|
FishQualityType.qualified_name(
|
||||||
|
|
@ -210,11 +192,7 @@ func refresh() -> void:
|
||||||
var was_selected: bool = _selected
|
var was_selected: bool = _selected
|
||||||
var was_empty: bool = _empty
|
var was_empty: bool = _empty
|
||||||
_selected = slot_index == _hotbar.get_selected_slot()
|
_selected = slot_index == _hotbar.get_selected_slot()
|
||||||
_empty = (
|
_empty = item == null and fish_catch == null
|
||||||
not _controller_preview_active
|
|
||||||
and item == null
|
|
||||||
and fish_catch == null
|
|
||||||
)
|
|
||||||
if was_selected != _selected or was_empty != _empty:
|
if was_selected != _selected or was_empty != _empty:
|
||||||
_apply_style()
|
_apply_style()
|
||||||
|
|
||||||
|
|
@ -242,9 +220,7 @@ func _apply_style() -> void:
|
||||||
var normal_fill: Color = profile.normal_fill
|
var normal_fill: Color = profile.normal_fill
|
||||||
normal_fill.a = 1.0
|
normal_fill.a = 1.0
|
||||||
var selected_fill: Color = normal_fill
|
var selected_fill: Color = normal_fill
|
||||||
if _controller_preview_active:
|
if _selected:
|
||||||
selected_fill = normal_fill.lightened(0.22)
|
|
||||||
elif _selected:
|
|
||||||
selected_fill = normal_fill.lightened(0.12)
|
selected_fill = normal_fill.lightened(0.12)
|
||||||
add_theme_stylebox_override(
|
add_theme_stylebox_override(
|
||||||
"normal",
|
"normal",
|
||||||
|
|
|
||||||
|
|
@ -42,53 +42,6 @@ func _ready() -> void:
|
||||||
_apply_icon_presentation(neutral_size)
|
_apply_icon_presentation(neutral_size)
|
||||||
|
|
||||||
|
|
||||||
func _gui_input(event: InputEvent) -> void:
|
|
||||||
if _adjustment_direction(self) == 0:
|
|
||||||
return
|
|
||||||
var direction: int = 0
|
|
||||||
if event.is_action_pressed(&"ui_left"):
|
|
||||||
direction = -1
|
|
||||||
elif event.is_action_pressed(&"ui_right"):
|
|
||||||
direction = 1
|
|
||||||
if direction == 0:
|
|
||||||
return
|
|
||||||
var adjustment_button: BaseButton = _find_adjustment_button(direction)
|
|
||||||
if adjustment_button == null:
|
|
||||||
return
|
|
||||||
adjustment_button.pressed.emit()
|
|
||||||
accept_event()
|
|
||||||
|
|
||||||
|
|
||||||
func _find_adjustment_button(direction: int) -> BaseButton:
|
|
||||||
var parent_node: Node = get_parent()
|
|
||||||
if parent_node == null:
|
|
||||||
return null
|
|
||||||
for child: Node in parent_node.get_children():
|
|
||||||
var button := child as BaseButton
|
|
||||||
if (
|
|
||||||
button != null
|
|
||||||
and button.visible
|
|
||||||
and not button.disabled
|
|
||||||
and _adjustment_direction(button) == direction
|
|
||||||
):
|
|
||||||
return button
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
func _adjustment_direction(button: BaseButton) -> int:
|
|
||||||
var descriptions: Array[String] = [
|
|
||||||
button.text.strip_edges().to_lower(),
|
|
||||||
button.tooltip_text.strip_edges().to_lower(),
|
|
||||||
button.accessibility_name.strip_edges().to_lower(),
|
|
||||||
]
|
|
||||||
for description: String in descriptions:
|
|
||||||
if description in ["-", "−", "minus", "decrease"]:
|
|
||||||
return -1
|
|
||||||
if description in ["+", "plus", "increase"]:
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
func apply_layout(
|
func apply_layout(
|
||||||
center: Vector2,
|
center: Vector2,
|
||||||
bubble_size: Vector2,
|
bubble_size: Vector2,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
class_name BubbleCluster
|
class_name BubbleCluster
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
const ControllerFocusNavigationType = preload(
|
|
||||||
"res://ui/controller_focus_navigation.gd"
|
|
||||||
)
|
|
||||||
|
|
||||||
@export var profile: BubbleMenuProfile
|
@export var profile: BubbleMenuProfile
|
||||||
@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0)
|
@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0)
|
||||||
@export var compact_reference_size: Vector2 = Vector2(294.0, 200.0)
|
@export var compact_reference_size: Vector2 = Vector2(294.0, 200.0)
|
||||||
|
|
@ -22,6 +18,14 @@ func configure(bubbles: Array[BubbleButton]) -> void:
|
||||||
if bubble.profile == null:
|
if bubble.profile == null:
|
||||||
bubble.profile = profile
|
bubble.profile = profile
|
||||||
bubble.apply_profile()
|
bubble.apply_profile()
|
||||||
|
if index > 0:
|
||||||
|
bubble.focus_neighbor_top = bubble.get_path_to(
|
||||||
|
_bubbles[index - 1]
|
||||||
|
)
|
||||||
|
if index + 1 < _bubbles.size():
|
||||||
|
bubble.focus_neighbor_bottom = bubble.get_path_to(
|
||||||
|
_bubbles[index + 1]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func apply_layout(field_size: Vector2, compact: bool) -> void:
|
func apply_layout(field_size: Vector2, compact: bool) -> void:
|
||||||
|
|
@ -48,7 +52,6 @@ func apply_layout(field_size: Vector2, compact: bool) -> void:
|
||||||
bubble_size,
|
bubble_size,
|
||||||
profile.font_size_ratio
|
profile.font_size_ratio
|
||||||
)
|
)
|
||||||
ControllerFocusNavigationType.configure_spatial_neighbors(_bubbles)
|
|
||||||
|
|
||||||
|
|
||||||
func advance_motion(delta: float) -> void:
|
func advance_motion(delta: float) -> void:
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ var _quality_color := Color.WHITE
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
set_meta(&"controller_focus_inversion_disabled", true)
|
|
||||||
resized.connect(_update_visual_pivot)
|
resized.connect(_update_visual_pivot)
|
||||||
focus_entered.connect(_refresh_style)
|
focus_entered.connect(_refresh_style)
|
||||||
focus_exited.connect(_refresh_style)
|
focus_exited.connect(_refresh_style)
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
class_name ControllerFocusNavigation
|
|
||||||
extends RefCounted
|
|
||||||
|
|
||||||
const PERPENDICULAR_WEIGHT: float = 2.5
|
|
||||||
const MINIMUM_FORWARD_DISTANCE: float = 0.5
|
|
||||||
|
|
||||||
|
|
||||||
static func configure_spatial_neighbors(controls: Array) -> void:
|
|
||||||
var candidates: Array[Control] = []
|
|
||||||
for item: Variant in controls:
|
|
||||||
var control := item as Control
|
|
||||||
if control == null or not control.is_visible_in_tree():
|
|
||||||
continue
|
|
||||||
candidates.append(control)
|
|
||||||
for control: Control in candidates:
|
|
||||||
control.focus_neighbor_left = _neighbor_path(
|
|
||||||
control, candidates, Vector2.LEFT
|
|
||||||
)
|
|
||||||
control.focus_neighbor_right = _neighbor_path(
|
|
||||||
control, candidates, Vector2.RIGHT
|
|
||||||
)
|
|
||||||
control.focus_neighbor_top = _neighbor_path(
|
|
||||||
control, candidates, Vector2.UP
|
|
||||||
)
|
|
||||||
control.focus_neighbor_bottom = _neighbor_path(
|
|
||||||
control, candidates, Vector2.DOWN
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
static func _neighbor_path(
|
|
||||||
origin: Control,
|
|
||||||
candidates: Array[Control],
|
|
||||||
direction: Vector2,
|
|
||||||
) -> NodePath:
|
|
||||||
var origin_center: Vector2 = origin.get_global_rect().get_center()
|
|
||||||
var best: Control = null
|
|
||||||
var best_score: float = INF
|
|
||||||
for candidate: Control in candidates:
|
|
||||||
if candidate == origin or candidate.focus_mode == Control.FOCUS_NONE:
|
|
||||||
continue
|
|
||||||
var delta: Vector2 = (
|
|
||||||
candidate.get_global_rect().get_center() - origin_center
|
|
||||||
)
|
|
||||||
var forward_distance: float = delta.dot(direction)
|
|
||||||
if forward_distance <= MINIMUM_FORWARD_DISTANCE:
|
|
||||||
continue
|
|
||||||
var perpendicular_distance: float = absf(delta.cross(direction))
|
|
||||||
var score: float = (
|
|
||||||
forward_distance
|
|
||||||
+ perpendicular_distance * PERPENDICULAR_WEIGHT
|
|
||||||
)
|
|
||||||
if score < best_score:
|
|
||||||
best = candidate
|
|
||||||
best_score = score
|
|
||||||
if best == null:
|
|
||||||
return NodePath()
|
|
||||||
return origin.get_path_to(best)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://dfb6bobetmujn
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
class_name ControllerFocusPresentation
|
|
||||||
extends Node
|
|
||||||
|
|
||||||
const CONTROLLER_MOTION_THRESHOLD: float = 0.35
|
|
||||||
const INVERSION_DISABLED_META: StringName = &"controller_focus_inversion_disabled"
|
|
||||||
|
|
||||||
var _controller_active: bool = false
|
|
||||||
var _focused_item: CanvasItem
|
|
||||||
var _original_material: Material
|
|
||||||
var _inversion_material: ShaderMaterial
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
var inversion_shader := Shader.new()
|
|
||||||
inversion_shader.code = """
|
|
||||||
shader_type canvas_item;
|
|
||||||
render_mode unshaded;
|
|
||||||
|
|
||||||
void fragment() {
|
|
||||||
vec4 source = texture(TEXTURE, UV) * COLOR;
|
|
||||||
COLOR = vec4(vec3(1.0) - source.rgb, source.a);
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
_inversion_material = ShaderMaterial.new()
|
|
||||||
_inversion_material.shader = inversion_shader
|
|
||||||
get_viewport().gui_focus_changed.connect(_on_focus_changed)
|
|
||||||
set_process_input(true)
|
|
||||||
set_process(true)
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
|
||||||
_restore_focused_item()
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventJoypadButton:
|
|
||||||
if (event as InputEventJoypadButton).pressed:
|
|
||||||
_set_controller_active(true)
|
|
||||||
elif event is InputEventJoypadMotion:
|
|
||||||
if absf((event as InputEventJoypadMotion).axis_value) >= (
|
|
||||||
CONTROLLER_MOTION_THRESHOLD
|
|
||||||
):
|
|
||||||
_set_controller_active(true)
|
|
||||||
elif event is InputEventMouseButton:
|
|
||||||
if (event as InputEventMouseButton).pressed:
|
|
||||||
_set_controller_active(false)
|
|
||||||
elif event is InputEventKey:
|
|
||||||
if (event as InputEventKey).pressed:
|
|
||||||
_set_controller_active(false)
|
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
|
||||||
if not is_instance_valid(_focused_item):
|
|
||||||
_focused_item = null
|
|
||||||
_original_material = null
|
|
||||||
return
|
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
if (
|
|
||||||
focus_owner != _focused_item
|
|
||||||
or not focus_owner.is_visible_in_tree()
|
|
||||||
or focus_owner.focus_mode == Control.FOCUS_NONE
|
|
||||||
or bool(focus_owner.get_meta(INVERSION_DISABLED_META, false))
|
|
||||||
):
|
|
||||||
_apply_to_focus(focus_owner)
|
|
||||||
|
|
||||||
|
|
||||||
func _set_controller_active(active: bool) -> void:
|
|
||||||
if _controller_active == active:
|
|
||||||
return
|
|
||||||
_controller_active = active
|
|
||||||
_apply_to_focus(get_viewport().gui_get_focus_owner())
|
|
||||||
|
|
||||||
|
|
||||||
func _on_focus_changed(control: Control) -> void:
|
|
||||||
_apply_to_focus(control)
|
|
||||||
|
|
||||||
|
|
||||||
func _apply_to_focus(control: Control) -> void:
|
|
||||||
_restore_focused_item()
|
|
||||||
if (
|
|
||||||
not _controller_active
|
|
||||||
or control == null
|
|
||||||
or not control.is_visible_in_tree()
|
|
||||||
or control.focus_mode == Control.FOCUS_NONE
|
|
||||||
or bool(control.get_meta(INVERSION_DISABLED_META, false))
|
|
||||||
):
|
|
||||||
return
|
|
||||||
_focused_item = control
|
|
||||||
_original_material = _focused_item.material
|
|
||||||
_focused_item.material = _inversion_material
|
|
||||||
|
|
||||||
|
|
||||||
func _restore_focused_item() -> void:
|
|
||||||
if is_instance_valid(_focused_item):
|
|
||||||
_focused_item.material = _original_material
|
|
||||||
_focused_item = null
|
|
||||||
_original_material = null
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://b2ccvcx1rfd1x
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
class_name ControllerFocusRecovery
|
|
||||||
extends Node
|
|
||||||
|
|
||||||
const CONTROLLER_AXIS_THRESHOLD: float = 0.35
|
|
||||||
const SEMANTIC_MATCH_BONUS: float = 1000000.0
|
|
||||||
|
|
||||||
var _controller_active: bool = false
|
|
||||||
var _last_focus_center: Vector2 = Vector2.ZERO
|
|
||||||
var _last_focus_key: String = ""
|
|
||||||
var _scope_chain: Array[WeakRef] = []
|
|
||||||
var _recovery_generation: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
set_process_input(true)
|
|
||||||
set_process(true)
|
|
||||||
get_viewport().gui_focus_changed.connect(_on_gui_focus_changed)
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
|
||||||
var viewport := get_viewport()
|
|
||||||
if viewport.gui_focus_changed.is_connected(_on_gui_focus_changed):
|
|
||||||
viewport.gui_focus_changed.disconnect(_on_gui_focus_changed)
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if event is InputEventJoypadButton:
|
|
||||||
var button_event := event as InputEventJoypadButton
|
|
||||||
if button_event.pressed:
|
|
||||||
_controller_active = true
|
|
||||||
if button_event.button_index == JOY_BUTTON_LEFT_SHOULDER:
|
|
||||||
_recovery_generation += 1
|
|
||||||
_scope_chain.clear()
|
|
||||||
return
|
|
||||||
if event is InputEventJoypadMotion:
|
|
||||||
if absf((event as InputEventJoypadMotion).axis_value) >= (
|
|
||||||
CONTROLLER_AXIS_THRESHOLD
|
|
||||||
):
|
|
||||||
_controller_active = true
|
|
||||||
return
|
|
||||||
if event is InputEventMouseButton:
|
|
||||||
if (event as InputEventMouseButton).pressed:
|
|
||||||
_controller_active = false
|
|
||||||
return
|
|
||||||
if event is InputEventKey and (event as InputEventKey).pressed:
|
|
||||||
_controller_active = false
|
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
|
||||||
if (
|
|
||||||
_controller_active
|
|
||||||
and not _scope_chain.is_empty()
|
|
||||||
and get_viewport().gui_get_focus_owner() == null
|
|
||||||
):
|
|
||||||
_recover_focus(_recovery_generation)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_gui_focus_changed(control: Control) -> void:
|
|
||||||
_recovery_generation += 1
|
|
||||||
if control != null:
|
|
||||||
if _controller_active and control is BaseButton:
|
|
||||||
_remember_focus(control)
|
|
||||||
return
|
|
||||||
if not _controller_active or _scope_chain.is_empty():
|
|
||||||
return
|
|
||||||
var generation: int = _recovery_generation
|
|
||||||
_recover_focus.call_deferred(generation)
|
|
||||||
|
|
||||||
|
|
||||||
func _remember_focus(control: Control) -> void:
|
|
||||||
_last_focus_center = control.get_global_rect().get_center()
|
|
||||||
_last_focus_key = _semantic_key(control)
|
|
||||||
_scope_chain.clear()
|
|
||||||
var recovery_root: Node = get_parent()
|
|
||||||
var ancestor: Node = control.get_parent()
|
|
||||||
while ancestor != null:
|
|
||||||
if ancestor is Control:
|
|
||||||
_scope_chain.append(weakref(ancestor))
|
|
||||||
if ancestor == recovery_root:
|
|
||||||
break
|
|
||||||
ancestor = ancestor.get_parent()
|
|
||||||
|
|
||||||
|
|
||||||
func _recover_focus(generation: int) -> void:
|
|
||||||
if generation != _recovery_generation:
|
|
||||||
return
|
|
||||||
if not _controller_active or get_viewport().gui_get_focus_owner() != null:
|
|
||||||
return
|
|
||||||
for scope_reference: WeakRef in _scope_chain:
|
|
||||||
var scope := scope_reference.get_ref() as Control
|
|
||||||
if (
|
|
||||||
scope == null
|
|
||||||
or not is_instance_valid(scope)
|
|
||||||
or not scope.is_inside_tree()
|
|
||||||
or not scope.is_visible_in_tree()
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
var replacement := _best_replacement_in(scope)
|
|
||||||
if replacement != null:
|
|
||||||
replacement.grab_focus()
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
func _best_replacement_in(scope: Control) -> Control:
|
|
||||||
var best: Control = null
|
|
||||||
var best_score: float = INF
|
|
||||||
for node: Node in scope.find_children("*", "Control", true, false):
|
|
||||||
var candidate := node as Control
|
|
||||||
if not _is_focusable(candidate):
|
|
||||||
continue
|
|
||||||
var distance: float = candidate.get_global_rect().get_center().distance_squared_to(
|
|
||||||
_last_focus_center
|
|
||||||
)
|
|
||||||
if not _last_focus_key.is_empty() and _semantic_key(candidate) == _last_focus_key:
|
|
||||||
distance -= SEMANTIC_MATCH_BONUS
|
|
||||||
if distance < best_score:
|
|
||||||
best = candidate
|
|
||||||
best_score = distance
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
func _is_focusable(control: Control) -> bool:
|
|
||||||
if (
|
|
||||||
control == null
|
|
||||||
or not control.is_inside_tree()
|
|
||||||
or not control.is_visible_in_tree()
|
|
||||||
or control.focus_mode == Control.FOCUS_NONE
|
|
||||||
):
|
|
||||||
return false
|
|
||||||
var button := control as BaseButton
|
|
||||||
return button == null or not button.disabled
|
|
||||||
|
|
||||||
|
|
||||||
func _semantic_key(control: Control) -> String:
|
|
||||||
if control.has_meta(&"controller_focus_key"):
|
|
||||||
return str(control.get_meta(&"controller_focus_key"))
|
|
||||||
var button := control as Button
|
|
||||||
var label: String = button.text if button != null else ""
|
|
||||||
var tooltip: String = control.tooltip_text
|
|
||||||
tooltip = tooltip.trim_suffix(" (show variants)")
|
|
||||||
tooltip = tooltip.trim_suffix(" (hide variants)")
|
|
||||||
return "%s|%s|%s" % [control.get_class(), label, tooltip]
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c7cw61ethr3pt
|
|
||||||
|
|
@ -8,7 +8,6 @@ const ControllerMappingManagerType = preload(
|
||||||
)
|
)
|
||||||
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||||
const CAPTURE_NEUTRAL_SECONDS: float = 0.22
|
const CAPTURE_NEUTRAL_SECONDS: float = 0.22
|
||||||
const CANCEL_COMBO_HOLD_SECONDS: float = 1.25
|
|
||||||
|
|
||||||
var _mapping_manager: ControllerMappingManagerType
|
var _mapping_manager: ControllerMappingManagerType
|
||||||
var _binding_buttons: Dictionary = {}
|
var _binding_buttons: Dictionary = {}
|
||||||
|
|
@ -25,9 +24,6 @@ var _auto_map_draft: Dictionary = {}
|
||||||
var _capture_device_id: int = 0
|
var _capture_device_id: int = 0
|
||||||
var _waiting_for_neutral: bool = false
|
var _waiting_for_neutral: bool = false
|
||||||
var _neutral_elapsed: float = 0.0
|
var _neutral_elapsed: float = 0.0
|
||||||
var _cancel_left_bumper_pressed: bool = false
|
|
||||||
var _cancel_right_bumper_pressed: bool = false
|
|
||||||
var _cancel_combo_elapsed: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -116,14 +112,6 @@ func _process(delta: float) -> void:
|
||||||
or _capturing_role.is_empty()
|
or _capturing_role.is_empty()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if _cancel_left_bumper_pressed and _cancel_right_bumper_pressed:
|
|
||||||
_cancel_combo_elapsed += delta
|
|
||||||
_progress_label.text = "keep holding both bumpers to cancel"
|
|
||||||
if _cancel_combo_elapsed >= CANCEL_COMBO_HOLD_SECONDS:
|
|
||||||
_cancel_capture()
|
|
||||||
_progress_label.text = "controller mapping cancelled"
|
|
||||||
return
|
|
||||||
_cancel_combo_elapsed = 0.0
|
|
||||||
if _waiting_for_neutral:
|
if _waiting_for_neutral:
|
||||||
if not _mapping_manager.are_capture_inputs_neutral(_capture_device_id):
|
if not _mapping_manager.are_capture_inputs_neutral(_capture_device_id):
|
||||||
_neutral_elapsed = 0.0
|
_neutral_elapsed = 0.0
|
||||||
|
|
@ -164,7 +152,18 @@ func _on_controller_input_observed(event: InputEvent) -> void:
|
||||||
return
|
return
|
||||||
if event_device != _capture_device_id:
|
if event_device != _capture_device_id:
|
||||||
return
|
return
|
||||||
_track_cancel_combo_button(button_event)
|
if (
|
||||||
|
button_event != null
|
||||||
|
and button_event.pressed
|
||||||
|
and _capturing_role != ControllerMappingManagerType.ROLE_B
|
||||||
|
and _mapping_manager.event_matches_role(
|
||||||
|
event,
|
||||||
|
ControllerMappingManagerType.ROLE_B,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_cancel_capture()
|
||||||
|
_progress_label.text = "controller mapping cancelled"
|
||||||
|
return
|
||||||
if _waiting_for_neutral:
|
if _waiting_for_neutral:
|
||||||
return
|
return
|
||||||
_try_capture_event(event)
|
_try_capture_event(event)
|
||||||
|
|
@ -333,7 +332,6 @@ func _begin_auto_map() -> void:
|
||||||
_auto_map_active = true
|
_auto_map_active = true
|
||||||
_auto_map_index = 0
|
_auto_map_index = 0
|
||||||
_auto_map_draft = {}
|
_auto_map_draft = {}
|
||||||
_reset_cancel_combo()
|
|
||||||
_capture_device_id = _mapping_manager.get_active_device_id()
|
_capture_device_id = _mapping_manager.get_active_device_id()
|
||||||
_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])
|
_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])
|
||||||
|
|
||||||
|
|
@ -342,7 +340,6 @@ func _begin_manual_capture(role: StringName) -> void:
|
||||||
_auto_map_active = false
|
_auto_map_active = false
|
||||||
_auto_map_index = -1
|
_auto_map_index = -1
|
||||||
_auto_map_draft.clear()
|
_auto_map_draft.clear()
|
||||||
_reset_cancel_combo()
|
|
||||||
_capture_device_id = _mapping_manager.get_active_device_id()
|
_capture_device_id = _mapping_manager.get_active_device_id()
|
||||||
_set_capture_role(role)
|
_set_capture_role(role)
|
||||||
|
|
||||||
|
|
@ -359,14 +356,10 @@ func _refresh_capture_prompt() -> void:
|
||||||
if _capturing_role.is_empty():
|
if _capturing_role.is_empty():
|
||||||
return
|
return
|
||||||
if _auto_map_active:
|
if _auto_map_active:
|
||||||
_progress_label.text = (
|
_progress_label.text = "step %d of %d: %s" % [
|
||||||
"step %d of %d: %s\n"
|
|
||||||
+ "hold both bumpers for %.2f seconds to cancel"
|
|
||||||
) % [
|
|
||||||
_auto_map_index + 1,
|
_auto_map_index + 1,
|
||||||
ControllerMappingManagerType.ROLE_ORDER.size(),
|
ControllerMappingManagerType.ROLE_ORDER.size(),
|
||||||
_mapping_manager.get_role_prompt(_capturing_role),
|
_mapping_manager.get_role_prompt(_capturing_role),
|
||||||
CANCEL_COMBO_HOLD_SECONDS,
|
|
||||||
]
|
]
|
||||||
return
|
return
|
||||||
var input_instruction: String = "press any controller button"
|
var input_instruction: String = "press any controller button"
|
||||||
|
|
@ -415,34 +408,11 @@ func _cancel_capture() -> void:
|
||||||
_auto_map_draft.clear()
|
_auto_map_draft.clear()
|
||||||
_waiting_for_neutral = false
|
_waiting_for_neutral = false
|
||||||
_neutral_elapsed = 0.0
|
_neutral_elapsed = 0.0
|
||||||
_reset_cancel_combo()
|
|
||||||
_set_action_buttons_disabled(false)
|
_set_action_buttons_disabled(false)
|
||||||
if _progress_label != null:
|
if _progress_label != null:
|
||||||
_progress_label.text = ""
|
_progress_label.text = ""
|
||||||
|
|
||||||
|
|
||||||
func _track_cancel_combo_button(button_event: InputEventJoypadButton) -> void:
|
|
||||||
if button_event == null:
|
|
||||||
return
|
|
||||||
match button_event.button_index:
|
|
||||||
JOY_BUTTON_LEFT_SHOULDER:
|
|
||||||
_cancel_left_bumper_pressed = button_event.pressed
|
|
||||||
JOY_BUTTON_RIGHT_SHOULDER:
|
|
||||||
_cancel_right_bumper_pressed = button_event.pressed
|
|
||||||
_:
|
|
||||||
return
|
|
||||||
if not (
|
|
||||||
_cancel_left_bumper_pressed and _cancel_right_bumper_pressed
|
|
||||||
):
|
|
||||||
_cancel_combo_elapsed = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func _reset_cancel_combo() -> void:
|
|
||||||
_cancel_left_bumper_pressed = false
|
|
||||||
_cancel_right_bumper_pressed = false
|
|
||||||
_cancel_combo_elapsed = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
func _set_action_buttons_disabled(disabled: bool) -> void:
|
func _set_action_buttons_disabled(disabled: bool) -> void:
|
||||||
if _auto_map_button != null:
|
if _auto_map_button != null:
|
||||||
_auto_map_button.disabled = disabled
|
_auto_map_button.disabled = disabled
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,6 @@ const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
|
||||||
@onready var _shop_prompt_key: Label = %ShopPromptKey
|
@onready var _shop_prompt_key: Label = %ShopPromptKey
|
||||||
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
|
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
|
||||||
var _shop_animalese_voice: AnimaleseVoiceType
|
var _shop_animalese_voice: AnimaleseVoiceType
|
||||||
var _shop_npc_player_in_range: bool = false
|
|
||||||
var _shop_npc_spoken_for_current_visit: bool = false
|
|
||||||
@onready var _effect_status: Label = %EffectStatus
|
@onready var _effect_status: Label = %EffectStatus
|
||||||
@onready var _chat_ui: ChatUIType = %ChatUI
|
@onready var _chat_ui: ChatUIType = %ChatUI
|
||||||
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
|
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
|
||||||
|
|
@ -209,18 +207,6 @@ func _ready() -> void:
|
||||||
_hotbar_ui.presentation_transition_finished.connect(
|
_hotbar_ui.presentation_transition_finished.connect(
|
||||||
_on_hotbar_presentation_transition_finished
|
_on_hotbar_presentation_transition_finished
|
||||||
)
|
)
|
||||||
_player_menu.controller_hotbar_placement_requested.connect(
|
|
||||||
_on_controller_hotbar_placement_requested
|
|
||||||
)
|
|
||||||
_player_menu.controller_hotbar_placement_ended.connect(
|
|
||||||
_on_controller_hotbar_placement_ended
|
|
||||||
)
|
|
||||||
_player_menu.controller_hotbar_management_requested.connect(
|
|
||||||
_on_controller_hotbar_management_requested
|
|
||||||
)
|
|
||||||
_player_menu.controller_hotbar_management_ended.connect(
|
|
||||||
_on_controller_hotbar_management_ended
|
|
||||||
)
|
|
||||||
_title_settings_panel.panel_visibility_changed.connect(
|
_title_settings_panel.panel_visibility_changed.connect(
|
||||||
_on_settings_visibility_changed
|
_on_settings_visibility_changed
|
||||||
)
|
)
|
||||||
|
|
@ -517,7 +503,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
|
||||||
_chat_ui.toggle_chat()
|
_chat_ui.toggle_chat()
|
||||||
return true
|
return true
|
||||||
if focus_pressed:
|
if focus_pressed:
|
||||||
_chat_ui.refocus_gameplay()
|
_chat_ui.toggle_focus()
|
||||||
return true
|
return true
|
||||||
if accept_pressed:
|
if accept_pressed:
|
||||||
return _chat_ui.request_virtual_keyboard()
|
return _chat_ui.request_virtual_keyboard()
|
||||||
|
|
@ -1316,8 +1302,6 @@ func _on_hud_bait_inventory_changed() -> void:
|
||||||
|
|
||||||
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
_gameplay_ui_enabled = enabled
|
_gameplay_ui_enabled = enabled
|
||||||
if enabled:
|
|
||||||
_try_start_shop_npc_speech()
|
|
||||||
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
||||||
_experience_presentation.visible = enabled
|
_experience_presentation.visible = enabled
|
||||||
_refresh_chat_availability()
|
_refresh_chat_availability()
|
||||||
|
|
@ -1378,6 +1362,7 @@ func set_shop_prompt_visible(
|
||||||
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
||||||
) -> void:
|
) -> void:
|
||||||
_apply_shop_prompt_style()
|
_apply_shop_prompt_style()
|
||||||
|
var was_visible := _shop_prompt.visible
|
||||||
_shop_prompt.visible = (
|
_shop_prompt.visible = (
|
||||||
is_visible
|
is_visible
|
||||||
and _gameplay_ui_enabled
|
and _gameplay_ui_enabled
|
||||||
|
|
@ -1386,28 +1371,7 @@ func set_shop_prompt_visible(
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
)
|
)
|
||||||
if _shop_prompt.visible:
|
if _shop_prompt.visible:
|
||||||
if world_anchor.is_finite():
|
if not was_visible:
|
||||||
_position_shop_prompt(world_anchor)
|
|
||||||
|
|
||||||
|
|
||||||
func set_shop_npc_player_in_range(in_range: bool) -> void:
|
|
||||||
if _shop_npc_player_in_range == in_range:
|
|
||||||
return
|
|
||||||
_shop_npc_player_in_range = in_range
|
|
||||||
if not in_range:
|
|
||||||
_shop_npc_spoken_for_current_visit = false
|
|
||||||
return
|
|
||||||
_try_start_shop_npc_speech()
|
|
||||||
|
|
||||||
|
|
||||||
func _try_start_shop_npc_speech() -> void:
|
|
||||||
if (
|
|
||||||
not _shop_npc_player_in_range
|
|
||||||
or _shop_npc_spoken_for_current_visit
|
|
||||||
or not _gameplay_ui_enabled
|
|
||||||
):
|
|
||||||
return
|
|
||||||
_shop_npc_spoken_for_current_visit = true
|
|
||||||
_ensure_shop_animalese_voice()
|
_ensure_shop_animalese_voice()
|
||||||
TypewriterRevealType.start(
|
TypewriterRevealType.start(
|
||||||
_shop_prompt_message,
|
_shop_prompt_message,
|
||||||
|
|
@ -1420,6 +1384,8 @@ func _try_start_shop_npc_speech() -> void:
|
||||||
SHOP_ANIMALESE_VOICE_ID,
|
SHOP_ANIMALESE_VOICE_ID,
|
||||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||||
)
|
)
|
||||||
|
if world_anchor.is_finite():
|
||||||
|
_position_shop_prompt(world_anchor)
|
||||||
|
|
||||||
|
|
||||||
func _ensure_shop_animalese_voice() -> void:
|
func _ensure_shop_animalese_voice() -> void:
|
||||||
|
|
@ -2051,48 +2017,6 @@ func _on_inventory_hotbar_context_changed(show_hotbar: bool) -> void:
|
||||||
_refresh_hotbar_visibility()
|
_refresh_hotbar_visibility()
|
||||||
|
|
||||||
|
|
||||||
func _on_controller_hotbar_placement_requested(
|
|
||||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
|
||||||
identity: StringName,
|
|
||||||
initial_slot: int,
|
|
||||||
) -> void:
|
|
||||||
_player_menu_hotbar_visible = true
|
|
||||||
_hotbar_ui.set_player_menu_context(true)
|
|
||||||
_hotbar_ui.set_drag_enabled(false)
|
|
||||||
_hotbar_ui.set_presentation_visible(true, false)
|
|
||||||
_hotbar_ui.begin_controller_placement(
|
|
||||||
assignment_kind,
|
|
||||||
identity,
|
|
||||||
initial_slot,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_controller_hotbar_placement_ended() -> void:
|
|
||||||
_hotbar_ui.end_controller_placement()
|
|
||||||
_hotbar_ui.set_drag_enabled(
|
|
||||||
_player_menu_open
|
|
||||||
and _player_menu_hotbar_visible
|
|
||||||
and not _system_menu_open
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_controller_hotbar_management_requested(initial_slot: int) -> void:
|
|
||||||
_player_menu_hotbar_visible = true
|
|
||||||
_hotbar_ui.set_player_menu_context(true)
|
|
||||||
_hotbar_ui.set_drag_enabled(false)
|
|
||||||
_hotbar_ui.set_presentation_visible(true, false)
|
|
||||||
_hotbar_ui.begin_controller_management(initial_slot)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_controller_hotbar_management_ended() -> void:
|
|
||||||
_hotbar_ui.end_controller_management()
|
|
||||||
_hotbar_ui.set_drag_enabled(
|
|
||||||
_player_menu_open
|
|
||||||
and _player_menu_hotbar_visible
|
|
||||||
and not _system_menu_open
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_hotbar_presentation_transition_finished(
|
func _on_hotbar_presentation_transition_finished(
|
||||||
is_visible: bool,
|
is_visible: bool,
|
||||||
) -> void:
|
) -> void:
|
||||||
|
|
|
||||||
128
ui/hotbar.gd
128
ui/hotbar.gd
|
|
@ -4,7 +4,6 @@ extends Control
|
||||||
signal presentation_transition_finished(is_visible: bool)
|
signal presentation_transition_finished(is_visible: bool)
|
||||||
|
|
||||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
const ItemDataType = preload("res://items/item_data.gd")
|
|
||||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
|
@ -42,13 +41,6 @@ var _item_name_suppressed: bool = false
|
||||||
var _motion_elapsed: float = 0.0
|
var _motion_elapsed: float = 0.0
|
||||||
var _compact_layout: bool = false
|
var _compact_layout: bool = false
|
||||||
var _player_menu_context: bool = false
|
var _player_menu_context: bool = false
|
||||||
var _controller_placement_active: bool = false
|
|
||||||
var _controller_management_active: bool = false
|
|
||||||
var _controller_placement_kind: PlayerHotbarType.AssignmentKind = (
|
|
||||||
PlayerHotbarType.AssignmentKind.EMPTY
|
|
||||||
)
|
|
||||||
var _controller_placement_identity: StringName
|
|
||||||
var _controller_placement_texture: Texture2D
|
|
||||||
var _visibility_tween: Tween
|
var _visibility_tween: Tween
|
||||||
var _visibility_generation: int = 0
|
var _visibility_generation: int = 0
|
||||||
|
|
||||||
|
|
@ -107,109 +99,6 @@ func set_drag_enabled(enabled: bool) -> void:
|
||||||
_hide_item_name()
|
_hide_item_name()
|
||||||
|
|
||||||
|
|
||||||
func begin_controller_placement(
|
|
||||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
|
||||||
identity: StringName,
|
|
||||||
initial_slot: int,
|
|
||||||
) -> void:
|
|
||||||
if _hotbar == null or identity.is_empty():
|
|
||||||
return
|
|
||||||
_controller_placement_active = true
|
|
||||||
_controller_placement_kind = assignment_kind
|
|
||||||
_controller_placement_identity = identity
|
|
||||||
_controller_placement_texture = _resolve_controller_placement_texture()
|
|
||||||
var slot_count: int = _slots.size()
|
|
||||||
for index: int in slot_count:
|
|
||||||
var slot: BubbleHotbarSlotType = _slots[index]
|
|
||||||
slot.focus_mode = Control.FOCUS_ALL
|
|
||||||
slot.focus_neighbor_left = slot.get_path_to(
|
|
||||||
_slots[wrapi(index - 1, 0, slot_count)]
|
|
||||||
)
|
|
||||||
slot.focus_neighbor_right = slot.get_path_to(
|
|
||||||
_slots[wrapi(index + 1, 0, slot_count)]
|
|
||||||
)
|
|
||||||
slot.focus_neighbor_top = slot.get_path_to(slot)
|
|
||||||
slot.focus_neighbor_bottom = slot.get_path_to(slot)
|
|
||||||
var target_index: int = clampi(initial_slot, 0, slot_count - 1)
|
|
||||||
_hotbar.select_slot(target_index)
|
|
||||||
_refresh_controller_placement_preview()
|
|
||||||
_slots[target_index].call_deferred("grab_focus")
|
|
||||||
|
|
||||||
|
|
||||||
func end_controller_placement() -> void:
|
|
||||||
if not _controller_placement_active:
|
|
||||||
return
|
|
||||||
_controller_placement_active = false
|
|
||||||
_controller_placement_kind = PlayerHotbarType.AssignmentKind.EMPTY
|
|
||||||
_controller_placement_identity = StringName()
|
|
||||||
_controller_placement_texture = null
|
|
||||||
for slot: BubbleHotbarSlotType in _slots:
|
|
||||||
slot.focus_mode = Control.FOCUS_NONE
|
|
||||||
slot.set_controller_placement_preview(false, null)
|
|
||||||
_show_selected_item_briefly()
|
|
||||||
|
|
||||||
|
|
||||||
func begin_controller_management(initial_slot: int) -> void:
|
|
||||||
if _hotbar == null or _slots.is_empty():
|
|
||||||
return
|
|
||||||
_controller_management_active = true
|
|
||||||
var slot_count: int = _slots.size()
|
|
||||||
for index: int in slot_count:
|
|
||||||
var slot: BubbleHotbarSlotType = _slots[index]
|
|
||||||
slot.focus_mode = Control.FOCUS_ALL
|
|
||||||
slot.focus_neighbor_left = slot.get_path_to(
|
|
||||||
_slots[wrapi(index - 1, 0, slot_count)]
|
|
||||||
)
|
|
||||||
slot.focus_neighbor_right = slot.get_path_to(
|
|
||||||
_slots[wrapi(index + 1, 0, slot_count)]
|
|
||||||
)
|
|
||||||
slot.focus_neighbor_top = slot.get_path_to(slot)
|
|
||||||
slot.focus_neighbor_bottom = slot.get_path_to(slot)
|
|
||||||
var target_index: int = clampi(initial_slot, 0, slot_count - 1)
|
|
||||||
_hotbar.select_slot(target_index)
|
|
||||||
_slots[target_index].call_deferred("grab_focus")
|
|
||||||
|
|
||||||
|
|
||||||
func end_controller_management() -> void:
|
|
||||||
if not _controller_management_active:
|
|
||||||
return
|
|
||||||
_controller_management_active = false
|
|
||||||
for slot: BubbleHotbarSlotType in _slots:
|
|
||||||
slot.focus_mode = Control.FOCUS_NONE
|
|
||||||
_show_selected_item_briefly()
|
|
||||||
|
|
||||||
|
|
||||||
func _resolve_controller_placement_texture() -> Texture2D:
|
|
||||||
if (
|
|
||||||
_controller_placement_kind == PlayerHotbarType.AssignmentKind.FISH
|
|
||||||
and _fish_inventory != null
|
|
||||||
):
|
|
||||||
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(
|
|
||||||
_controller_placement_identity
|
|
||||||
)
|
|
||||||
return fish_catch.fish.display_texture if fish_catch != null else null
|
|
||||||
if (
|
|
||||||
_controller_placement_kind == PlayerHotbarType.AssignmentKind.ITEM
|
|
||||||
and _catalog != null
|
|
||||||
):
|
|
||||||
var item: ItemDataType = _catalog.get_item_by_id(
|
|
||||||
_controller_placement_identity
|
|
||||||
)
|
|
||||||
return item.icon if item != null else null
|
|
||||||
return null
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_controller_placement_preview() -> void:
|
|
||||||
if not _controller_placement_active or _hotbar == null:
|
|
||||||
return
|
|
||||||
var target_index: int = _hotbar.get_selected_slot()
|
|
||||||
for slot: BubbleHotbarSlotType in _slots:
|
|
||||||
slot.set_controller_placement_preview(
|
|
||||||
slot.slot_index == target_index,
|
|
||||||
_controller_placement_texture,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func set_player_menu_context(enabled: bool) -> void:
|
func set_player_menu_context(enabled: bool) -> void:
|
||||||
if _player_menu_context == enabled:
|
if _player_menu_context == enabled:
|
||||||
return
|
return
|
||||||
|
|
@ -341,9 +230,6 @@ func _collect_slots() -> void:
|
||||||
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
|
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
|
||||||
slot.item_drag_started.connect(_on_slot_drag_started)
|
slot.item_drag_started.connect(_on_slot_drag_started)
|
||||||
slot.item_drag_finished.connect(_on_slot_drag_finished)
|
slot.item_drag_finished.connect(_on_slot_drag_finished)
|
||||||
slot.focus_entered.connect(
|
|
||||||
_on_controller_slot_focused.bind(slot.slot_index)
|
|
||||||
)
|
|
||||||
_slots.append(slot)
|
_slots.append(slot)
|
||||||
_slots.sort_custom(
|
_slots.sort_custom(
|
||||||
func(
|
func(
|
||||||
|
|
@ -392,24 +278,10 @@ func _on_selected_slot_changed(
|
||||||
_item_id: StringName,
|
_item_id: StringName,
|
||||||
) -> void:
|
) -> void:
|
||||||
_refresh()
|
_refresh()
|
||||||
if _controller_placement_active:
|
|
||||||
_refresh_controller_placement_preview()
|
|
||||||
return
|
|
||||||
if _hovered_slot_index < 0:
|
if _hovered_slot_index < 0:
|
||||||
_show_selected_item_briefly()
|
_show_selected_item_briefly()
|
||||||
|
|
||||||
|
|
||||||
func _on_controller_slot_focused(slot_index: int) -> void:
|
|
||||||
if (
|
|
||||||
not (_controller_placement_active or _controller_management_active)
|
|
||||||
or _hotbar == null
|
|
||||||
):
|
|
||||||
return
|
|
||||||
_hotbar.select_slot(slot_index)
|
|
||||||
if _controller_placement_active:
|
|
||||||
_refresh_controller_placement_preview()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_slot_item_hovered(
|
func _on_slot_item_hovered(
|
||||||
slot_index: int,
|
slot_index: int,
|
||||||
item_id: StringName,
|
item_id: StringName,
|
||||||
|
|
|
||||||
|
|
@ -453,7 +453,6 @@ func _refresh_catalog() -> void:
|
||||||
|
|
||||||
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
||||||
var entry := Button.new()
|
var entry := Button.new()
|
||||||
entry.set_meta(&"controller_focus_inversion_disabled", true)
|
|
||||||
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
||||||
entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
entry.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
entry.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
|
|
||||||
|
|
@ -1,456 +0,0 @@
|
||||||
class_name OnScreenKeyboard
|
|
||||||
extends Control
|
|
||||||
|
|
||||||
const ControllerFocusNavigationType = preload(
|
|
||||||
"res://ui/controller_focus_navigation.gd"
|
|
||||||
)
|
|
||||||
const CHECK_ICON: Texture2D = preload(
|
|
||||||
"res://ui/icons/pictograms/check_mark_dark.png"
|
|
||||||
)
|
|
||||||
const CHARACTER_KEY_SIZE: Vector2 = Vector2(96.0, 72.0)
|
|
||||||
const CHARACTER_FONT_SIZE: int = 34
|
|
||||||
const TRIGGER_PRESS_THRESHOLD: float = 0.55
|
|
||||||
const TRIGGER_RELEASE_THRESHOLD: float = 0.25
|
|
||||||
|
|
||||||
signal text_submitted(value: String)
|
|
||||||
|
|
||||||
enum Page {
|
|
||||||
LOWER,
|
|
||||||
UPPER,
|
|
||||||
SYMBOLS,
|
|
||||||
}
|
|
||||||
|
|
||||||
var _enabled: bool = false
|
|
||||||
var _page: Page = Page.LOWER
|
|
||||||
var _target: Control
|
|
||||||
var _target_virtual_keyboard_enabled: bool = true
|
|
||||||
var _buffer: String = ""
|
|
||||||
var _preview: Label
|
|
||||||
var _page_buttons: Array[Button] = []
|
|
||||||
var _keys_host: VBoxContainer
|
|
||||||
var _key_buttons: Array[Button] = []
|
|
||||||
var _caret_left_button: Button
|
|
||||||
var _caret_right_button: Button
|
|
||||||
var _backspace_button: Button
|
|
||||||
var _space_button: Button
|
|
||||||
var _check_button: Button
|
|
||||||
var _left_trigger_pressed: bool = false
|
|
||||||
var _right_trigger_pressed: bool = false
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
|
||||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
||||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
z_index = 1000
|
|
||||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
|
||||||
_build_interface()
|
|
||||||
hide()
|
|
||||||
set_process_input(true)
|
|
||||||
|
|
||||||
|
|
||||||
func set_enabled(enabled: bool) -> void:
|
|
||||||
_enabled = enabled
|
|
||||||
if not _enabled and visible:
|
|
||||||
_close_keyboard(false)
|
|
||||||
|
|
||||||
|
|
||||||
func is_enabled() -> bool:
|
|
||||||
return _enabled
|
|
||||||
|
|
||||||
|
|
||||||
func is_open() -> bool:
|
|
||||||
return visible
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
|
||||||
if not _enabled:
|
|
||||||
return
|
|
||||||
if visible:
|
|
||||||
var joy_motion := event as InputEventJoypadMotion
|
|
||||||
if joy_motion != null and _handle_trigger_shortcut(joy_motion):
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
var joy_button := event as InputEventJoypadButton
|
|
||||||
if joy_button != null and joy_button.pressed:
|
|
||||||
if joy_button.button_index == JOY_BUTTON_X:
|
|
||||||
_backspace()
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if joy_button.button_index == JOY_BUTTON_LEFT_SHOULDER:
|
|
||||||
_close_keyboard(false)
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if joy_button.button_index == JOY_BUTTON_RIGHT_SHOULDER:
|
|
||||||
_set_page(wrapi(int(_page) + 1, 0, Page.size()))
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if event.is_action_pressed(&"ui_cancel"):
|
|
||||||
_close_keyboard(true)
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
var joy_event := event as InputEventJoypadButton
|
|
||||||
if (
|
|
||||||
joy_event == null
|
|
||||||
or not joy_event.pressed
|
|
||||||
or (
|
|
||||||
joy_event.button_index != JOY_BUTTON_A
|
|
||||||
and not event.is_action_pressed(&"ui_accept")
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return
|
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
if _can_edit(focus_owner):
|
|
||||||
_open_for(focus_owner)
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
|
|
||||||
|
|
||||||
func _can_edit(control: Control) -> bool:
|
|
||||||
if control is LineEdit:
|
|
||||||
return (control as LineEdit).editable
|
|
||||||
if control is TextEdit:
|
|
||||||
return (control as TextEdit).editable
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _open_for(control: Control) -> void:
|
|
||||||
_target = control
|
|
||||||
_target_virtual_keyboard_enabled = bool(
|
|
||||||
_target.get("virtual_keyboard_enabled")
|
|
||||||
)
|
|
||||||
_target.set("virtual_keyboard_enabled", false)
|
|
||||||
_buffer = str(_target.get("text"))
|
|
||||||
_page = Page.LOWER
|
|
||||||
show()
|
|
||||||
_refresh_preview()
|
|
||||||
_rebuild_keys()
|
|
||||||
|
|
||||||
|
|
||||||
func _close_keyboard(restore_focus: bool) -> void:
|
|
||||||
var prior_target: Control = _target
|
|
||||||
if is_instance_valid(prior_target):
|
|
||||||
prior_target.set(
|
|
||||||
"virtual_keyboard_enabled",
|
|
||||||
_target_virtual_keyboard_enabled
|
|
||||||
)
|
|
||||||
hide()
|
|
||||||
get_viewport().gui_release_focus()
|
|
||||||
_target = null
|
|
||||||
if restore_focus and is_instance_valid(prior_target):
|
|
||||||
prior_target.grab_focus()
|
|
||||||
|
|
||||||
|
|
||||||
func _submit() -> void:
|
|
||||||
var submitted_target: Control = _target
|
|
||||||
var submitted_text: String = _buffer
|
|
||||||
_close_keyboard(false)
|
|
||||||
if submitted_target is LineEdit:
|
|
||||||
(submitted_target as LineEdit).text_submitted.emit(submitted_text)
|
|
||||||
elif submitted_target is TextEdit:
|
|
||||||
(submitted_target as TextEdit).text_changed.emit()
|
|
||||||
text_submitted.emit(submitted_text)
|
|
||||||
|
|
||||||
|
|
||||||
func _type_character(character: String) -> void:
|
|
||||||
if not is_instance_valid(_target):
|
|
||||||
_close_keyboard(false)
|
|
||||||
return
|
|
||||||
var caret: int = _get_caret_column()
|
|
||||||
var next_text: String = (
|
|
||||||
_buffer.substr(0, caret)
|
|
||||||
+ character
|
|
||||||
+ _buffer.substr(caret)
|
|
||||||
)
|
|
||||||
if _target is LineEdit:
|
|
||||||
var line_edit := _target as LineEdit
|
|
||||||
if line_edit.max_length > 0 and next_text.length() > line_edit.max_length:
|
|
||||||
return
|
|
||||||
_buffer = next_text
|
|
||||||
_set_target_text(caret + character.length())
|
|
||||||
|
|
||||||
|
|
||||||
func _type_space() -> void:
|
|
||||||
_type_character(" ")
|
|
||||||
|
|
||||||
|
|
||||||
func _backspace() -> void:
|
|
||||||
if not is_instance_valid(_target) or _buffer.is_empty():
|
|
||||||
return
|
|
||||||
var caret: int = _get_caret_column()
|
|
||||||
if caret <= 0:
|
|
||||||
return
|
|
||||||
_buffer = _buffer.erase(caret - 1, 1)
|
|
||||||
_set_target_text(caret - 1)
|
|
||||||
|
|
||||||
|
|
||||||
func _move_caret(direction: int) -> void:
|
|
||||||
if not is_instance_valid(_target):
|
|
||||||
_close_keyboard(false)
|
|
||||||
return
|
|
||||||
var caret: int = clampi(
|
|
||||||
_get_caret_column() + direction,
|
|
||||||
0,
|
|
||||||
_buffer.length(),
|
|
||||||
)
|
|
||||||
if _target is LineEdit:
|
|
||||||
(_target as LineEdit).caret_column = caret
|
|
||||||
elif _target is TextEdit:
|
|
||||||
(_target as TextEdit).set_caret_column(caret)
|
|
||||||
_refresh_preview()
|
|
||||||
|
|
||||||
|
|
||||||
func _handle_trigger_shortcut(event: InputEventJoypadMotion) -> bool:
|
|
||||||
if event.axis == JOY_AXIS_TRIGGER_LEFT:
|
|
||||||
if event.axis_value <= TRIGGER_RELEASE_THRESHOLD:
|
|
||||||
_left_trigger_pressed = false
|
|
||||||
elif (
|
|
||||||
event.axis_value >= TRIGGER_PRESS_THRESHOLD
|
|
||||||
and not _left_trigger_pressed
|
|
||||||
):
|
|
||||||
_left_trigger_pressed = true
|
|
||||||
_move_caret(-1)
|
|
||||||
return true
|
|
||||||
elif event.axis == JOY_AXIS_TRIGGER_RIGHT:
|
|
||||||
if event.axis_value <= TRIGGER_RELEASE_THRESHOLD:
|
|
||||||
_right_trigger_pressed = false
|
|
||||||
elif (
|
|
||||||
event.axis_value >= TRIGGER_PRESS_THRESHOLD
|
|
||||||
and not _right_trigger_pressed
|
|
||||||
):
|
|
||||||
_right_trigger_pressed = true
|
|
||||||
_move_caret(1)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _get_caret_column() -> int:
|
|
||||||
if _target is LineEdit:
|
|
||||||
return clampi(
|
|
||||||
(_target as LineEdit).caret_column,
|
|
||||||
0,
|
|
||||||
_buffer.length()
|
|
||||||
)
|
|
||||||
return _buffer.length()
|
|
||||||
|
|
||||||
|
|
||||||
func _set_target_text(caret: int) -> void:
|
|
||||||
if _target is LineEdit:
|
|
||||||
var line_edit := _target as LineEdit
|
|
||||||
line_edit.text = _buffer
|
|
||||||
line_edit.caret_column = caret
|
|
||||||
line_edit.text_changed.emit(_buffer)
|
|
||||||
elif _target is TextEdit:
|
|
||||||
var text_edit := _target as TextEdit
|
|
||||||
text_edit.text = _buffer
|
|
||||||
text_edit.text_changed.emit()
|
|
||||||
_refresh_preview()
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_preview() -> void:
|
|
||||||
if _preview == null:
|
|
||||||
return
|
|
||||||
var displayed_text: String = _buffer
|
|
||||||
if _target is LineEdit and (_target as LineEdit).secret:
|
|
||||||
displayed_text = "*".repeat(_buffer.length())
|
|
||||||
var caret: int = clampi(_get_caret_column(), 0, displayed_text.length())
|
|
||||||
_preview.text = displayed_text.insert(caret, "|")
|
|
||||||
|
|
||||||
|
|
||||||
func _set_page(page_index: int) -> void:
|
|
||||||
_page = page_index as Page
|
|
||||||
_rebuild_keys()
|
|
||||||
|
|
||||||
|
|
||||||
func _rebuild_keys() -> void:
|
|
||||||
for child: Node in _keys_host.get_children():
|
|
||||||
_keys_host.remove_child(child)
|
|
||||||
child.queue_free()
|
|
||||||
_key_buttons.clear()
|
|
||||||
var rows: Array = _rows_for_page()
|
|
||||||
for row_value: Variant in rows:
|
|
||||||
var row := HBoxContainer.new()
|
|
||||||
row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
row.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
||||||
row.add_theme_constant_override("separation", 10)
|
|
||||||
_keys_host.add_child(row)
|
|
||||||
for character_value: Variant in row_value as Array:
|
|
||||||
var character: String = str(character_value)
|
|
||||||
var key_button: Button = _make_button(character, CHARACTER_KEY_SIZE)
|
|
||||||
key_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
|
||||||
key_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
||||||
key_button.add_theme_font_size_override(
|
|
||||||
"font_size", CHARACTER_FONT_SIZE
|
|
||||||
)
|
|
||||||
key_button.pressed.connect(_type_character.bind(character))
|
|
||||||
row.add_child(key_button)
|
|
||||||
_key_buttons.append(key_button)
|
|
||||||
for index: int in _page_buttons.size():
|
|
||||||
_page_buttons[index].button_pressed = index == int(_page)
|
|
||||||
call_deferred("_configure_key_focus")
|
|
||||||
|
|
||||||
|
|
||||||
func _configure_key_focus() -> void:
|
|
||||||
var focus_controls: Array[Control] = []
|
|
||||||
for button: Button in _key_buttons:
|
|
||||||
focus_controls.append(button)
|
|
||||||
for button: Button in [
|
|
||||||
_caret_left_button,
|
|
||||||
_caret_right_button,
|
|
||||||
_backspace_button,
|
|
||||||
_space_button,
|
|
||||||
_check_button,
|
|
||||||
]:
|
|
||||||
focus_controls.append(button)
|
|
||||||
ControllerFocusNavigationType.configure_spatial_neighbors(focus_controls)
|
|
||||||
if not _key_buttons.is_empty():
|
|
||||||
_key_buttons[0].grab_focus()
|
|
||||||
|
|
||||||
|
|
||||||
func _rows_for_page() -> Array:
|
|
||||||
match _page:
|
|
||||||
Page.UPPER:
|
|
||||||
return [
|
|
||||||
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
|
|
||||||
["A", "S", "D", "F", "G", "H", "J", "K", "L"],
|
|
||||||
["Z", "X", "C", "V", "B", "N", "M"],
|
|
||||||
]
|
|
||||||
Page.SYMBOLS:
|
|
||||||
return [
|
|
||||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
|
||||||
["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"],
|
|
||||||
["-", "_", "=", "+", "[", "]", "{", "}"],
|
|
||||||
[".", ",", "?", "/", ":", ";", "'", "\""]
|
|
||||||
]
|
|
||||||
_:
|
|
||||||
return [
|
|
||||||
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
|
|
||||||
["a", "s", "d", "f", "g", "h", "j", "k", "l"],
|
|
||||||
["z", "x", "c", "v", "b", "n", "m"],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
func _build_interface() -> void:
|
|
||||||
var backdrop := ColorRect.new()
|
|
||||||
backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
backdrop.color = Color(0.012, 0.075, 0.105, 0.82)
|
|
||||||
backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
|
|
||||||
add_child(backdrop)
|
|
||||||
var screen_margin := MarginContainer.new()
|
|
||||||
screen_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
||||||
for side: StringName in [
|
|
||||||
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"
|
|
||||||
]:
|
|
||||||
screen_margin.add_theme_constant_override(side, 14)
|
|
||||||
add_child(screen_margin)
|
|
||||||
var panel := PanelContainer.new()
|
|
||||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
||||||
panel.add_theme_stylebox_override(
|
|
||||||
"panel",
|
|
||||||
_make_style(Color(0.075, 0.27, 0.34, 0.98), 18, 4)
|
|
||||||
)
|
|
||||||
screen_margin.add_child(panel)
|
|
||||||
var margin := MarginContainer.new()
|
|
||||||
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
|
|
||||||
margin.add_theme_constant_override(side, 18)
|
|
||||||
panel.add_child(margin)
|
|
||||||
var layout := VBoxContainer.new()
|
|
||||||
layout.add_theme_constant_override("separation", 10)
|
|
||||||
margin.add_child(layout)
|
|
||||||
var preview_panel := PanelContainer.new()
|
|
||||||
preview_panel.custom_minimum_size = Vector2(0.0, 86.0)
|
|
||||||
preview_panel.add_theme_stylebox_override(
|
|
||||||
"panel",
|
|
||||||
_make_style(Color(0.82, 0.94, 0.95, 1.0), 10, 2)
|
|
||||||
)
|
|
||||||
layout.add_child(preview_panel)
|
|
||||||
_preview = Label.new()
|
|
||||||
_preview.add_theme_color_override("font_color", Color(0.025, 0.12, 0.17, 1.0))
|
|
||||||
_preview.add_theme_font_size_override("font_size", 30)
|
|
||||||
_preview.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
|
||||||
_preview.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
||||||
_preview.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
|
||||||
_preview.add_theme_constant_override("outline_size", 0)
|
|
||||||
preview_panel.add_child(_preview)
|
|
||||||
var page_row := HBoxContainer.new()
|
|
||||||
page_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
page_row.add_theme_constant_override("separation", 12)
|
|
||||||
layout.add_child(page_row)
|
|
||||||
for page_name: String in ["lower", "upper", "symbols"]:
|
|
||||||
var page_button: Button = _make_button(page_name, Vector2(150.0, 44.0))
|
|
||||||
page_button.toggle_mode = true
|
|
||||||
page_button.focus_mode = Control.FOCUS_NONE
|
|
||||||
page_button.pressed.connect(_set_page.bind(_page_buttons.size()))
|
|
||||||
page_row.add_child(page_button)
|
|
||||||
_page_buttons.append(page_button)
|
|
||||||
_keys_host = VBoxContainer.new()
|
|
||||||
_keys_host.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
||||||
_keys_host.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
_keys_host.add_theme_constant_override("separation", 12)
|
|
||||||
layout.add_child(_keys_host)
|
|
||||||
var utility_row := HBoxContainer.new()
|
|
||||||
utility_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
|
||||||
utility_row.add_theme_constant_override("separation", 12)
|
|
||||||
layout.add_child(utility_row)
|
|
||||||
_caret_left_button = _make_button("LT <", Vector2(118.0, 64.0))
|
|
||||||
_caret_left_button.tooltip_text = "move text cursor left"
|
|
||||||
_caret_left_button.pressed.connect(_move_caret.bind(-1))
|
|
||||||
utility_row.add_child(_caret_left_button)
|
|
||||||
_caret_right_button = _make_button("> RT", Vector2(118.0, 64.0))
|
|
||||||
_caret_right_button.tooltip_text = "move text cursor right"
|
|
||||||
_caret_right_button.pressed.connect(_move_caret.bind(1))
|
|
||||||
utility_row.add_child(_caret_right_button)
|
|
||||||
_backspace_button = _make_button("backspace X", Vector2(180.0, 64.0))
|
|
||||||
_backspace_button.pressed.connect(_backspace)
|
|
||||||
utility_row.add_child(_backspace_button)
|
|
||||||
_space_button = _make_button("space", Vector2(350.0, 64.0))
|
|
||||||
_space_button.size_flags_stretch_ratio = 4.0
|
|
||||||
_space_button.pressed.connect(_type_space)
|
|
||||||
utility_row.add_child(_space_button)
|
|
||||||
_check_button = _make_button("", Vector2(92.0, 64.0))
|
|
||||||
_check_button.icon = CHECK_ICON
|
|
||||||
_check_button.expand_icon = true
|
|
||||||
_check_button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
_check_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
_check_button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
||||||
_check_button.tooltip_text = "submit"
|
|
||||||
_check_button.accessibility_name = "submit text"
|
|
||||||
_check_button.add_theme_constant_override("icon_max_width", 42)
|
|
||||||
_check_button.pressed.connect(_submit)
|
|
||||||
utility_row.add_child(_check_button)
|
|
||||||
_rebuild_keys()
|
|
||||||
|
|
||||||
|
|
||||||
func _make_button(label: String, minimum_size: Vector2) -> Button:
|
|
||||||
var button := Button.new()
|
|
||||||
button.text = label
|
|
||||||
button.custom_minimum_size = minimum_size
|
|
||||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
||||||
button.focus_mode = Control.FOCUS_ALL
|
|
||||||
button.add_theme_font_size_override("font_size", 22)
|
|
||||||
button.add_theme_color_override("font_color", Color(0.025, 0.12, 0.17, 1.0))
|
|
||||||
button.add_theme_color_override("font_hover_color", Color(0.025, 0.12, 0.17, 1.0))
|
|
||||||
button.add_theme_color_override("font_focus_color", Color(0.025, 0.12, 0.17, 1.0))
|
|
||||||
button.add_theme_stylebox_override(
|
|
||||||
"normal",
|
|
||||||
_make_style(Color(0.72, 0.88, 0.91, 1.0), 10, 2)
|
|
||||||
)
|
|
||||||
button.add_theme_stylebox_override(
|
|
||||||
"hover",
|
|
||||||
_make_style(Color(0.87, 0.96, 0.96, 1.0), 10, 3)
|
|
||||||
)
|
|
||||||
button.add_theme_stylebox_override("focus", button.get_theme_stylebox("hover"))
|
|
||||||
button.add_theme_stylebox_override(
|
|
||||||
"pressed",
|
|
||||||
_make_style(Color(0.44, 0.72, 0.77, 1.0), 10, 3)
|
|
||||||
)
|
|
||||||
return button
|
|
||||||
|
|
||||||
|
|
||||||
func _make_style(color: Color, radius: int, border_width: int) -> StyleBoxFlat:
|
|
||||||
var style := StyleBoxFlat.new()
|
|
||||||
style.bg_color = color
|
|
||||||
style.border_color = Color(0.025, 0.12, 0.17, 1.0)
|
|
||||||
style.set_border_width_all(border_width)
|
|
||||||
style.set_corner_radius_all(radius)
|
|
||||||
return style
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://dcanibf8hordh
|
|
||||||
|
|
@ -85,14 +85,6 @@ const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
|
||||||
|
|
||||||
signal menu_visibility_changed(is_open: bool)
|
signal menu_visibility_changed(is_open: bool)
|
||||||
signal inventory_hotbar_context_changed(show_hotbar: bool)
|
signal inventory_hotbar_context_changed(show_hotbar: bool)
|
||||||
signal controller_hotbar_placement_requested(
|
|
||||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
|
||||||
identity: StringName,
|
|
||||||
initial_slot: int,
|
|
||||||
)
|
|
||||||
signal controller_hotbar_placement_ended
|
|
||||||
signal controller_hotbar_management_requested(initial_slot: int)
|
|
||||||
signal controller_hotbar_management_ended
|
|
||||||
signal menu_exit_started
|
signal menu_exit_started
|
||||||
signal shop_cooler_modal_changed(is_open: bool)
|
signal shop_cooler_modal_changed(is_open: bool)
|
||||||
|
|
||||||
|
|
@ -132,13 +124,6 @@ enum CloseReason {
|
||||||
TEARDOWN,
|
TEARDOWN,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ControllerOwnership {
|
|
||||||
ITEM_LIST,
|
|
||||||
NOTEPAD_ACTIONS,
|
|
||||||
HOTBAR_MANAGEMENT,
|
|
||||||
HOTBAR_PLACEMENT,
|
|
||||||
}
|
|
||||||
|
|
||||||
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
|
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
|
||||||
const INVENTORY_MAIN_SIZE := Vector2(882.0, 484.0)
|
const INVENTORY_MAIN_SIZE := Vector2(882.0, 484.0)
|
||||||
const INVENTORY_PANEL_GAP := 16.0
|
const INVENTORY_PANEL_GAP := 16.0
|
||||||
|
|
@ -317,15 +302,6 @@ var _last_inventory_section: Section = Section.COOLER
|
||||||
var _bag_view: BagView = BagView.EQUIPMENT
|
var _bag_view: BagView = BagView.EQUIPMENT
|
||||||
var _tackle_view: TackleView = TackleView.BAIT
|
var _tackle_view: TackleView = TackleView.BAIT
|
||||||
var _selected_tackle_item_id: StringName
|
var _selected_tackle_item_id: StringName
|
||||||
var _controller_ownership: ControllerOwnership = ControllerOwnership.ITEM_LIST
|
|
||||||
var _controller_source_section: Section = Section.COOLER
|
|
||||||
var _controller_source_identity: StringName
|
|
||||||
var _controller_notepad_actions: Array[BaseButton] = []
|
|
||||||
var _controller_hotbar_assignment_kind: PlayerHotbarType.AssignmentKind = (
|
|
||||||
PlayerHotbarType.AssignmentKind.EMPTY
|
|
||||||
)
|
|
||||||
var _controller_hotbar_identity: StringName
|
|
||||||
var _controller_previous_hotbar_slot: int = 0
|
|
||||||
var _sort_mode: SortMode = SortMode.CATCH_ORDER
|
var _sort_mode: SortMode = SortMode.CATCH_ORDER
|
||||||
var _sort_descending: bool = true
|
var _sort_descending: bool = true
|
||||||
var _fish_selection := FishBatchSelectionType.new()
|
var _fish_selection := FishBatchSelectionType.new()
|
||||||
|
|
@ -613,25 +589,12 @@ func set_profile_preview_world_pixel_size(pixel_size: int) -> void:
|
||||||
_profile_page.set_world_pixel_size(pixel_size)
|
_profile_page.set_world_pixel_size(pixel_size)
|
||||||
|
|
||||||
|
|
||||||
var _left_page_trigger_held: bool = false
|
|
||||||
var _right_page_trigger_held: bool = false
|
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.echo:
|
if event is InputEventKey and event.echo:
|
||||||
return
|
return
|
||||||
if _handle_controller_ownership_input(event):
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if visible:
|
|
||||||
_reserve_main_navigation_for_page_switching()
|
|
||||||
_reserve_visible_secondary_navigation()
|
|
||||||
if _handle_controller_page_switch(event):
|
if _handle_controller_page_switch(event):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
if _handle_controller_secondary_switch(event):
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if _handle_direct_page_shortcut(event):
|
if _handle_direct_page_shortcut(event):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
@ -650,381 +613,47 @@ func _input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|
||||||
if not visible:
|
|
||||||
return false
|
|
||||||
var button_event := event as InputEventJoypadButton
|
|
||||||
var accept_pressed: bool = (
|
|
||||||
button_event != null
|
|
||||||
and button_event.pressed
|
|
||||||
and _event_matches_controller_role(
|
|
||||||
event,
|
|
||||||
ControllerMappingManagerType.ROLE_A,
|
|
||||||
JOY_BUTTON_A,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
var cancel_pressed: bool = (
|
|
||||||
button_event != null
|
|
||||||
and button_event.pressed
|
|
||||||
and _event_matches_controller_role(
|
|
||||||
event,
|
|
||||||
ControllerMappingManagerType.ROLE_B,
|
|
||||||
JOY_BUTTON_B,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
var alternate_pressed: bool = (
|
|
||||||
button_event != null
|
|
||||||
and button_event.pressed
|
|
||||||
and _event_matches_controller_role(
|
|
||||||
event,
|
|
||||||
ControllerMappingManagerType.ROLE_Y,
|
|
||||||
JOY_BUTTON_Y,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
|
|
||||||
if cancel_pressed:
|
|
||||||
_release_controller_ownership(true, false)
|
|
||||||
return true
|
|
||||||
if alternate_pressed:
|
|
||||||
if _hotbar != null:
|
|
||||||
_hotbar.clear_slot(_hotbar.get_selected_slot())
|
|
||||||
return true
|
|
||||||
if accept_pressed:
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
if _controller_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
|
||||||
if accept_pressed:
|
|
||||||
_confirm_controller_hotbar_placement()
|
|
||||||
return true
|
|
||||||
if cancel_pressed:
|
|
||||||
_release_controller_ownership(true, true)
|
|
||||||
return true
|
|
||||||
if alternate_pressed:
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
if _controller_ownership == ControllerOwnership.NOTEPAD_ACTIONS:
|
|
||||||
if cancel_pressed:
|
|
||||||
_release_controller_ownership(true, false)
|
|
||||||
return true
|
|
||||||
if alternate_pressed:
|
|
||||||
return true
|
|
||||||
var direction: int = 0
|
|
||||||
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
|
|
||||||
direction = -1
|
|
||||||
elif (
|
|
||||||
event.is_action_pressed("ui_right")
|
|
||||||
or event.is_action_pressed("ui_down")
|
|
||||||
):
|
|
||||||
direction = 1
|
|
||||||
if direction != 0:
|
|
||||||
_focus_next_notepad_action(direction)
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
if alternate_pressed:
|
|
||||||
return _try_begin_controller_hotbar_placement()
|
|
||||||
if accept_pressed:
|
|
||||||
return _try_enter_notepad_controller_ownership()
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _event_matches_controller_role(
|
|
||||||
event: InputEvent,
|
|
||||||
role: StringName,
|
|
||||||
fallback_button: JoyButton,
|
|
||||||
) -> bool:
|
|
||||||
if _controller_mapping_manager != null:
|
|
||||||
return _controller_mapping_manager.event_matches_role(event, role)
|
|
||||||
var button_event := event as InputEventJoypadButton
|
|
||||||
return button_event != null and button_event.button_index == fallback_button
|
|
||||||
|
|
||||||
|
|
||||||
func _try_enter_notepad_controller_ownership() -> bool:
|
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
if focus_owner == null:
|
|
||||||
return false
|
|
||||||
var source_identity: StringName
|
|
||||||
var actions: Array[BaseButton] = []
|
|
||||||
if _current_section == Section.COOLER:
|
|
||||||
var fish_node := focus_owner as CoolerFishSpriteType
|
|
||||||
if fish_node == null or fish_node.catch_id.is_empty():
|
|
||||||
return false
|
|
||||||
source_identity = fish_node.catch_id
|
|
||||||
_on_catch_card_pressed(source_identity)
|
|
||||||
if not _favorite_bubble.disabled:
|
|
||||||
actions.append(_favorite_bubble)
|
|
||||||
if not _sell_bubble.disabled:
|
|
||||||
actions.append(_sell_bubble)
|
|
||||||
elif _current_section == Section.TACKLE_BOX:
|
|
||||||
if not focus_owner.has_meta(&"controller_tackle_item_id"):
|
|
||||||
return false
|
|
||||||
source_identity = StringName(
|
|
||||||
str(focus_owner.get_meta(&"controller_tackle_item_id"))
|
|
||||||
)
|
|
||||||
_select_tackle_item(source_identity)
|
|
||||||
if _tackle_equip_button.visible and not _tackle_equip_button.disabled:
|
|
||||||
actions.append(_tackle_equip_button)
|
|
||||||
else:
|
|
||||||
return false
|
|
||||||
if actions.is_empty():
|
|
||||||
_restore_controller_item_focus(_current_section, source_identity)
|
|
||||||
return true
|
|
||||||
_controller_source_section = _current_section
|
|
||||||
_controller_source_identity = source_identity
|
|
||||||
_controller_notepad_actions = actions
|
|
||||||
_controller_ownership = ControllerOwnership.NOTEPAD_ACTIONS
|
|
||||||
actions.front().call_deferred("grab_focus")
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _focus_next_notepad_action(direction: int) -> void:
|
|
||||||
var available: Array[BaseButton] = []
|
|
||||||
for action: BaseButton in _controller_notepad_actions:
|
|
||||||
if (
|
|
||||||
is_instance_valid(action)
|
|
||||||
and action.visible
|
|
||||||
and not action.disabled
|
|
||||||
and action.focus_mode != Control.FOCUS_NONE
|
|
||||||
):
|
|
||||||
available.append(action)
|
|
||||||
if available.is_empty():
|
|
||||||
return
|
|
||||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
var current_index: int = available.find(focused)
|
|
||||||
if current_index < 0:
|
|
||||||
current_index = 0 if direction > 0 else available.size() - 1
|
|
||||||
else:
|
|
||||||
current_index = wrapi(
|
|
||||||
current_index + direction,
|
|
||||||
0,
|
|
||||||
available.size(),
|
|
||||||
)
|
|
||||||
available[current_index].grab_focus()
|
|
||||||
|
|
||||||
|
|
||||||
func _try_begin_controller_hotbar_placement() -> bool:
|
|
||||||
if _hotbar == null:
|
|
||||||
return false
|
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
if focus_owner == null:
|
|
||||||
return false
|
|
||||||
var assignment_kind: PlayerHotbarType.AssignmentKind = (
|
|
||||||
PlayerHotbarType.AssignmentKind.EMPTY
|
|
||||||
)
|
|
||||||
var identity: StringName
|
|
||||||
if _current_section == Section.COOLER:
|
|
||||||
var fish_node := focus_owner as CoolerFishSpriteType
|
|
||||||
if fish_node == null or fish_node.catch_id.is_empty():
|
|
||||||
return false
|
|
||||||
assignment_kind = PlayerHotbarType.AssignmentKind.FISH
|
|
||||||
identity = fish_node.catch_id
|
|
||||||
elif _current_section == Section.BAG:
|
|
||||||
var item_node := focus_owner as BagItemSpriteType
|
|
||||||
if item_node == null or item_node.item_id.is_empty():
|
|
||||||
return false
|
|
||||||
var item: ItemDataType = (
|
|
||||||
_item_catalog.get_item_by_id(item_node.item_id)
|
|
||||||
if _item_catalog != null
|
|
||||||
else null
|
|
||||||
)
|
|
||||||
if item == null or not item.hotbar_allowed:
|
|
||||||
return true
|
|
||||||
assignment_kind = PlayerHotbarType.AssignmentKind.ITEM
|
|
||||||
identity = item_node.item_id
|
|
||||||
else:
|
|
||||||
return false
|
|
||||||
_controller_source_section = _current_section
|
|
||||||
_controller_source_identity = identity
|
|
||||||
_controller_hotbar_assignment_kind = assignment_kind
|
|
||||||
_controller_hotbar_identity = identity
|
|
||||||
_controller_previous_hotbar_slot = _hotbar.get_selected_slot()
|
|
||||||
var initial_slot: int = _find_controller_hotbar_assignment(
|
|
||||||
assignment_kind,
|
|
||||||
identity,
|
|
||||||
)
|
|
||||||
if initial_slot < 0:
|
|
||||||
initial_slot = _controller_previous_hotbar_slot
|
|
||||||
_controller_ownership = ControllerOwnership.HOTBAR_PLACEMENT
|
|
||||||
controller_hotbar_placement_requested.emit(
|
|
||||||
assignment_kind,
|
|
||||||
identity,
|
|
||||||
initial_slot,
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _find_controller_hotbar_assignment(
|
|
||||||
assignment_kind: PlayerHotbarType.AssignmentKind,
|
|
||||||
identity: StringName,
|
|
||||||
) -> int:
|
|
||||||
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
|
|
||||||
if (
|
|
||||||
assignment_kind == PlayerHotbarType.AssignmentKind.FISH
|
|
||||||
and _hotbar.get_fish_catch_id(slot_index) == identity
|
|
||||||
):
|
|
||||||
return slot_index
|
|
||||||
if (
|
|
||||||
assignment_kind == PlayerHotbarType.AssignmentKind.ITEM
|
|
||||||
and _hotbar.get_item_id(slot_index) == identity
|
|
||||||
):
|
|
||||||
return slot_index
|
|
||||||
return -1
|
|
||||||
|
|
||||||
|
|
||||||
func _confirm_controller_hotbar_placement() -> void:
|
|
||||||
if (
|
|
||||||
_controller_ownership != ControllerOwnership.HOTBAR_PLACEMENT
|
|
||||||
or _hotbar == null
|
|
||||||
):
|
|
||||||
return
|
|
||||||
var slot_index: int = _hotbar.get_selected_slot()
|
|
||||||
var assigned: bool = false
|
|
||||||
if (
|
|
||||||
_controller_hotbar_assignment_kind
|
|
||||||
== PlayerHotbarType.AssignmentKind.FISH
|
|
||||||
):
|
|
||||||
assigned = _hotbar.assign_fish(
|
|
||||||
slot_index,
|
|
||||||
_controller_hotbar_identity,
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
_controller_hotbar_assignment_kind
|
|
||||||
== PlayerHotbarType.AssignmentKind.ITEM
|
|
||||||
):
|
|
||||||
assigned = _hotbar.assign_item(
|
|
||||||
slot_index,
|
|
||||||
_controller_hotbar_identity,
|
|
||||||
)
|
|
||||||
if assigned:
|
|
||||||
_release_controller_ownership(true, false)
|
|
||||||
|
|
||||||
|
|
||||||
func _release_controller_ownership(
|
|
||||||
restore_source_focus: bool,
|
|
||||||
restore_previous_hotbar_slot: bool,
|
|
||||||
) -> void:
|
|
||||||
if _controller_ownership == ControllerOwnership.ITEM_LIST:
|
|
||||||
return
|
|
||||||
var prior_ownership: ControllerOwnership = _controller_ownership
|
|
||||||
var source_section: Section = _controller_source_section
|
|
||||||
var source_identity: StringName = _controller_source_identity
|
|
||||||
_controller_ownership = ControllerOwnership.ITEM_LIST
|
|
||||||
_controller_notepad_actions.clear()
|
|
||||||
_controller_source_identity = StringName()
|
|
||||||
_controller_hotbar_assignment_kind = PlayerHotbarType.AssignmentKind.EMPTY
|
|
||||||
_controller_hotbar_identity = StringName()
|
|
||||||
if prior_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
|
||||||
if restore_previous_hotbar_slot and _hotbar != null:
|
|
||||||
_hotbar.select_slot(_controller_previous_hotbar_slot)
|
|
||||||
controller_hotbar_placement_ended.emit()
|
|
||||||
if prior_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
|
|
||||||
controller_hotbar_management_ended.emit()
|
|
||||||
inventory_hotbar_context_changed.emit(
|
|
||||||
_current_section in [Section.COOLER, Section.BAG]
|
|
||||||
)
|
|
||||||
if restore_source_focus:
|
|
||||||
call_deferred(
|
|
||||||
"_restore_controller_item_focus",
|
|
||||||
source_section,
|
|
||||||
source_identity,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _restore_controller_item_focus(
|
|
||||||
section: Section,
|
|
||||||
identity: StringName,
|
|
||||||
) -> void:
|
|
||||||
if not visible or section != _current_section:
|
|
||||||
return
|
|
||||||
if identity.is_empty():
|
|
||||||
_focus_current_section()
|
|
||||||
return
|
|
||||||
var target: Control
|
|
||||||
if section == Section.COOLER:
|
|
||||||
target = _fish_nodes.get(identity) as CoolerFishSpriteType
|
|
||||||
elif section == Section.BAG:
|
|
||||||
target = _bag_item_nodes.get(identity) as BagItemSpriteType
|
|
||||||
elif section == Section.TACKLE_BOX:
|
|
||||||
for child: Node in _tackle_item_list.get_children():
|
|
||||||
var button := child as BaseButton
|
|
||||||
if (
|
|
||||||
button != null
|
|
||||||
and button.has_meta(&"controller_tackle_item_id")
|
|
||||||
and StringName(str(button.get_meta(
|
|
||||||
&"controller_tackle_item_id"
|
|
||||||
))) == identity
|
|
||||||
):
|
|
||||||
target = button
|
|
||||||
break
|
|
||||||
if (
|
|
||||||
target != null
|
|
||||||
and is_instance_valid(target)
|
|
||||||
and target.is_visible_in_tree()
|
|
||||||
and target.focus_mode != Control.FOCUS_NONE
|
|
||||||
):
|
|
||||||
target.grab_focus()
|
|
||||||
else:
|
|
||||||
_focus_current_section()
|
|
||||||
|
|
||||||
|
|
||||||
func _handle_controller_page_switch(event: InputEvent) -> bool:
|
func _handle_controller_page_switch(event: InputEvent) -> bool:
|
||||||
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||||
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
|
|
||||||
var use_mapping: bool = (
|
var use_mapping: bool = (
|
||||||
_controller_mapping_manager != null
|
_controller_mapping_manager != null
|
||||||
)
|
)
|
||||||
var uses_left_trigger: bool = (
|
var uses_left_bumper: bool = (
|
||||||
_controller_mapping_manager.event_uses_role(
|
_controller_mapping_manager.event_uses_role(
|
||||||
event, ControllerMappingManagerType.ROLE_POINTER_MODIFIER
|
event, ControllerMappingManagerType.ROLE_LB
|
||||||
)
|
)
|
||||||
if use_mapping
|
if use_mapping
|
||||||
else (
|
else (
|
||||||
motion_event != null
|
button_event != null
|
||||||
and motion_event.axis == JOY_AXIS_TRIGGER_LEFT
|
and button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
var uses_right_trigger: bool = (
|
var uses_right_bumper: bool = (
|
||||||
_controller_mapping_manager.event_uses_role(
|
_controller_mapping_manager.event_uses_role(
|
||||||
event, ControllerMappingManagerType.ROLE_CAMERA_ZOOM
|
event, ControllerMappingManagerType.ROLE_RB
|
||||||
)
|
)
|
||||||
if use_mapping
|
if use_mapping
|
||||||
else (
|
else (
|
||||||
motion_event != null
|
button_event != null
|
||||||
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
|
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not (uses_left_trigger or uses_right_trigger)
|
button_event == null
|
||||||
|
or not (uses_left_bumper or uses_right_bumper)
|
||||||
or not visible
|
or not visible
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
var is_pressed: bool = (
|
if not button_event.pressed:
|
||||||
button_event.pressed
|
|
||||||
if button_event != null
|
|
||||||
else motion_event.axis_value > 0.5
|
|
||||||
)
|
|
||||||
if not is_pressed:
|
|
||||||
if uses_left_trigger:
|
|
||||||
_left_page_trigger_held = false
|
|
||||||
if uses_right_trigger:
|
|
||||||
_right_page_trigger_held = false
|
|
||||||
return true
|
return true
|
||||||
if (
|
# Shoulder input belongs to the Player Menu while it is visible, even when
|
||||||
(uses_left_trigger and _left_page_trigger_held)
|
# a transition or modal temporarily prevents changing pages. This keeps LB
|
||||||
or (uses_right_trigger and _right_page_trigger_held)
|
# from opening Chat and RB from leaking into gameplay behind the menu.
|
||||||
):
|
|
||||||
return true
|
|
||||||
if uses_left_trigger:
|
|
||||||
_left_page_trigger_held = true
|
|
||||||
if uses_right_trigger:
|
|
||||||
_right_page_trigger_held = true
|
|
||||||
# Trigger input belongs to the Player Menu while it is visible, even when a
|
|
||||||
# transition or modal temporarily prevents changing pages.
|
|
||||||
if (
|
if (
|
||||||
_transitioning
|
_transitioning
|
||||||
or _page_transitioning
|
or _page_transitioning
|
||||||
or _sale_confirmation.visible
|
or _sale_confirmation.visible
|
||||||
or get_viewport().gui_is_dragging()
|
or get_viewport().gui_is_dragging()
|
||||||
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
|
||||||
):
|
):
|
||||||
return true
|
return true
|
||||||
var sections: Array[Section] = [
|
var sections: Array[Section] = [
|
||||||
|
|
@ -1041,169 +670,13 @@ func _handle_controller_page_switch(event: InputEvent) -> bool:
|
||||||
if current_index < 0:
|
if current_index < 0:
|
||||||
current_index = 0
|
current_index = 0
|
||||||
var direction: int = (
|
var direction: int = (
|
||||||
-1 if uses_left_trigger else 1
|
-1 if uses_left_bumper else 1
|
||||||
)
|
)
|
||||||
var next_index: int = wrapi(current_index + direction, 0, sections.size())
|
var next_index: int = wrapi(current_index + direction, 0, sections.size())
|
||||||
_show_section(sections[next_index])
|
_show_section(sections[next_index])
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _handle_controller_secondary_switch(event: InputEvent) -> bool:
|
|
||||||
var button_event := event as InputEventJoypadButton
|
|
||||||
if button_event == null or not visible:
|
|
||||||
return false
|
|
||||||
var use_mapping: bool = _controller_mapping_manager != null
|
|
||||||
var uses_left_bumper: bool = (
|
|
||||||
_controller_mapping_manager.event_uses_role(
|
|
||||||
event, ControllerMappingManagerType.ROLE_LB
|
|
||||||
)
|
|
||||||
if use_mapping
|
|
||||||
else button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
|
|
||||||
)
|
|
||||||
var uses_right_bumper: bool = (
|
|
||||||
_controller_mapping_manager.event_uses_role(
|
|
||||||
event, ControllerMappingManagerType.ROLE_RB
|
|
||||||
)
|
|
||||||
if use_mapping
|
|
||||||
else button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
|
||||||
)
|
|
||||||
if not (uses_left_bumper or uses_right_bumper):
|
|
||||||
return false
|
|
||||||
if not button_event.pressed:
|
|
||||||
return true
|
|
||||||
var direction: int = -1 if uses_left_bumper else 1
|
|
||||||
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
|
|
||||||
_cycle_from_controller_hotbar_management(direction)
|
|
||||||
return true
|
|
||||||
if (
|
|
||||||
_transitioning
|
|
||||||
or _page_transitioning
|
|
||||||
or _sale_confirmation.visible
|
|
||||||
or get_viewport().gui_is_dragging()
|
|
||||||
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
|
||||||
):
|
|
||||||
return true
|
|
||||||
if _is_inventory_section(_current_section):
|
|
||||||
if (
|
|
||||||
(direction > 0 and _current_section == Section.BAG)
|
|
||||||
or (direction < 0 and _current_section == Section.COOLER)
|
|
||||||
):
|
|
||||||
_begin_controller_hotbar_management()
|
|
||||||
return true
|
|
||||||
var inventory_sections: Array[Section] = [
|
|
||||||
Section.COOLER,
|
|
||||||
Section.TACKLE_BOX,
|
|
||||||
Section.BAG,
|
|
||||||
]
|
|
||||||
var inventory_index: int = inventory_sections.find(_current_section)
|
|
||||||
_show_section(inventory_sections[wrapi(
|
|
||||||
inventory_index + direction,
|
|
||||||
0,
|
|
||||||
inventory_sections.size(),
|
|
||||||
)])
|
|
||||||
return true
|
|
||||||
_cycle_visible_secondary_tabs(direction)
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _begin_controller_hotbar_management() -> void:
|
|
||||||
if _hotbar == null:
|
|
||||||
return
|
|
||||||
_controller_source_section = _current_section
|
|
||||||
_controller_source_identity = StringName()
|
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
if _current_section == Section.COOLER:
|
|
||||||
var fish_node := focus_owner as CoolerFishSpriteType
|
|
||||||
if fish_node != null:
|
|
||||||
_controller_source_identity = fish_node.catch_id
|
|
||||||
elif _current_section == Section.BAG:
|
|
||||||
var item_node := focus_owner as BagItemSpriteType
|
|
||||||
if item_node != null:
|
|
||||||
_controller_source_identity = item_node.item_id
|
|
||||||
_controller_ownership = ControllerOwnership.HOTBAR_MANAGEMENT
|
|
||||||
controller_hotbar_management_requested.emit(_hotbar.get_selected_slot())
|
|
||||||
|
|
||||||
|
|
||||||
func _cycle_from_controller_hotbar_management(direction: int) -> void:
|
|
||||||
_release_controller_ownership(false, false)
|
|
||||||
var target_section: Section = (
|
|
||||||
Section.COOLER if direction > 0 else Section.BAG
|
|
||||||
)
|
|
||||||
if target_section == _current_section:
|
|
||||||
call_deferred("_focus_current_section")
|
|
||||||
else:
|
|
||||||
_show_section(target_section)
|
|
||||||
|
|
||||||
|
|
||||||
func _cycle_visible_secondary_tabs(direction: int) -> bool:
|
|
||||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
|
||||||
var grouped_buttons: Dictionary = {}
|
|
||||||
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
|
||||||
var selected_group: Variant = null
|
|
||||||
var selected_group_y: float = INF
|
|
||||||
for group_value: Variant in grouped_buttons.keys():
|
|
||||||
var buttons := grouped_buttons[group_value] as Array
|
|
||||||
if buttons.size() < 2:
|
|
||||||
continue
|
|
||||||
var group_y: float = INF
|
|
||||||
for item: Variant in buttons:
|
|
||||||
var button := item as BaseButton
|
|
||||||
group_y = minf(group_y, button.global_position.y)
|
|
||||||
if group_y < selected_group_y:
|
|
||||||
selected_group = group_value
|
|
||||||
selected_group_y = group_y
|
|
||||||
if selected_group == null:
|
|
||||||
return false
|
|
||||||
var tabs := grouped_buttons[selected_group] as Array
|
|
||||||
tabs.sort_custom(func(first: BaseButton, second: BaseButton) -> bool:
|
|
||||||
return first.global_position.x < second.global_position.x
|
|
||||||
)
|
|
||||||
var current_index: int = 0
|
|
||||||
for index: int in tabs.size():
|
|
||||||
var tab := tabs[index] as BaseButton
|
|
||||||
if tab.button_pressed:
|
|
||||||
current_index = index
|
|
||||||
break
|
|
||||||
var target := tabs[wrapi(
|
|
||||||
current_index + direction, 0, tabs.size()
|
|
||||||
)] as BaseButton
|
|
||||||
target.set_pressed_no_signal(true)
|
|
||||||
target.pressed.emit()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _collect_visible_toggle_buttons(
|
|
||||||
root: Node,
|
|
||||||
navigation_cluster: Control,
|
|
||||||
grouped_buttons: Dictionary,
|
|
||||||
) -> void:
|
|
||||||
for child: Node in root.get_children():
|
|
||||||
var child_control := child as Control
|
|
||||||
if child_control != null and not child_control.is_visible_in_tree():
|
|
||||||
continue
|
|
||||||
var button := child as BaseButton
|
|
||||||
if (
|
|
||||||
button != null
|
|
||||||
and button.toggle_mode
|
|
||||||
and (
|
|
||||||
navigation_cluster == null
|
|
||||||
or not navigation_cluster.is_ancestor_of(button)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
var group_key: Variant = (
|
|
||||||
button.button_group
|
|
||||||
if button.button_group != null
|
|
||||||
else button.get_parent()
|
|
||||||
)
|
|
||||||
if not grouped_buttons.has(group_key):
|
|
||||||
grouped_buttons[group_key] = []
|
|
||||||
var buttons := grouped_buttons[group_key] as Array
|
|
||||||
buttons.append(button)
|
|
||||||
_collect_visible_toggle_buttons(
|
|
||||||
child, navigation_cluster, grouped_buttons
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
|
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
|
||||||
var key_event := event as InputEventKey
|
var key_event := event as InputEventKey
|
||||||
if (
|
if (
|
||||||
|
|
@ -1299,7 +772,6 @@ func open_menu() -> void:
|
||||||
or not _fishing_spot.can_open_player_menu()
|
or not _fishing_spot.can_open_player_menu()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
_release_controller_ownership(false, true)
|
|
||||||
_menu_generation += 1
|
_menu_generation += 1
|
||||||
_transition_generation += 1
|
_transition_generation += 1
|
||||||
_cancel_presentation_tween()
|
_cancel_presentation_tween()
|
||||||
|
|
@ -1319,9 +791,6 @@ func open_menu() -> void:
|
||||||
_update_shell_layout()
|
_update_shell_layout()
|
||||||
_begin_menu_entry()
|
_begin_menu_entry()
|
||||||
menu_visibility_changed.emit(true)
|
menu_visibility_changed.emit(true)
|
||||||
inventory_hotbar_context_changed.emit(
|
|
||||||
_current_section in [Section.COOLER, Section.BAG]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func open_section(section: Section) -> bool:
|
func open_section(section: Section) -> bool:
|
||||||
|
|
@ -1452,7 +921,6 @@ func close_menu(
|
||||||
if reason != CloseReason.USER:
|
if reason != CloseReason.USER:
|
||||||
_finish_close(reason, restore_controls, _menu_generation)
|
_finish_close(reason, restore_controls, _menu_generation)
|
||||||
return
|
return
|
||||||
_release_controller_ownership(false, true)
|
|
||||||
get_viewport().gui_cancel_drag()
|
get_viewport().gui_cancel_drag()
|
||||||
_close_sale_confirmation()
|
_close_sale_confirmation()
|
||||||
if reason in [
|
if reason in [
|
||||||
|
|
@ -1481,7 +949,6 @@ func close_for_session_end() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
_release_controller_ownership(false, true)
|
|
||||||
_cancel_presentation_tween()
|
_cancel_presentation_tween()
|
||||||
_cancel_page_tween()
|
_cancel_page_tween()
|
||||||
if visible:
|
if visible:
|
||||||
|
|
@ -1540,7 +1007,6 @@ func _show_section(section: Section) -> void:
|
||||||
and _profile_page.request_close_confirmation()
|
and _profile_page.request_close_confirmation()
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
_release_controller_ownership(false, true)
|
|
||||||
_begin_page_transition(section)
|
_begin_page_transition(section)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1656,39 +1122,22 @@ func _focus_current_section() -> void:
|
||||||
if _shop_cooler_context_active:
|
if _shop_cooler_context_active:
|
||||||
_focus_shop_cooler()
|
_focus_shop_cooler()
|
||||||
return
|
return
|
||||||
_reserve_main_navigation_for_page_switching()
|
if _current_section == Section.COOLER:
|
||||||
_reserve_visible_secondary_navigation()
|
_cooler_sub_tab.grab_focus()
|
||||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
elif _current_section == Section.BAG:
|
||||||
var candidates: Array[Control] = []
|
_bag_sub_tab.grab_focus()
|
||||||
_collect_focusable_content(self, navigation_cluster, candidates)
|
elif _current_section == Section.TACKLE_BOX:
|
||||||
if candidates.is_empty():
|
_tackle_sub_tab.grab_focus()
|
||||||
return
|
elif _current_section == Section.LOGBOOK:
|
||||||
candidates.sort_custom(func(first: Control, second: Control) -> bool:
|
_catalog_logbook.focus_initial()
|
||||||
if not is_equal_approx(first.global_position.y, second.global_position.y):
|
elif _current_section == Section.NET:
|
||||||
return first.global_position.y < second.global_position.y
|
_the_net_page.focus_initial()
|
||||||
return first.global_position.x < second.global_position.x
|
elif _current_section == Section.MAIL:
|
||||||
)
|
_mail_tab.grab_focus()
|
||||||
candidates[0].grab_focus()
|
elif _current_section == Section.PLAYERS:
|
||||||
|
_players_tab.grab_focus()
|
||||||
|
else:
|
||||||
func _collect_focusable_content(
|
_profile_tab.grab_focus()
|
||||||
root: Node,
|
|
||||||
navigation_cluster: Control,
|
|
||||||
output: Array[Control],
|
|
||||||
) -> void:
|
|
||||||
for child: Node in root.get_children():
|
|
||||||
if child == navigation_cluster:
|
|
||||||
continue
|
|
||||||
var control := child as Control
|
|
||||||
if control != null and not control.is_visible_in_tree():
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
control != null
|
|
||||||
and control.focus_mode != Control.FOCUS_NONE
|
|
||||||
and not control is ScrollBar
|
|
||||||
):
|
|
||||||
output.append(control)
|
|
||||||
_collect_focusable_content(child, navigation_cluster, output)
|
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
|
@ -1712,7 +1161,6 @@ func _process(delta: float) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _configure_navigation_focus() -> void:
|
func _configure_navigation_focus() -> void:
|
||||||
_reserve_main_navigation_for_page_switching()
|
|
||||||
var navigation: Array[BubbleButtonType] = [
|
var navigation: Array[BubbleButtonType] = [
|
||||||
_inventory_tab,
|
_inventory_tab,
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
|
|
@ -1734,48 +1182,6 @@ func _configure_navigation_focus() -> void:
|
||||||
bubble.focus_neighbor_right = bubble.get_path_to(next)
|
bubble.focus_neighbor_right = bubble.get_path_to(next)
|
||||||
bubble.focus_neighbor_top = bubble.focus_neighbor_left
|
bubble.focus_neighbor_top = bubble.focus_neighbor_left
|
||||||
bubble.focus_neighbor_bottom = bubble.focus_neighbor_right
|
bubble.focus_neighbor_bottom = bubble.focus_neighbor_right
|
||||||
|
|
||||||
|
|
||||||
func _reserve_main_navigation_for_page_switching() -> void:
|
|
||||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
|
||||||
if navigation_cluster == null:
|
|
||||||
return
|
|
||||||
_set_descendant_focus_disabled(navigation_cluster)
|
|
||||||
|
|
||||||
|
|
||||||
func _set_descendant_focus_disabled(root: Node) -> void:
|
|
||||||
for child: Node in root.get_children():
|
|
||||||
var control := child as Control
|
|
||||||
if control != null:
|
|
||||||
control.focus_mode = Control.FOCUS_NONE
|
|
||||||
_set_descendant_focus_disabled(child)
|
|
||||||
|
|
||||||
|
|
||||||
func _reserve_visible_secondary_navigation() -> void:
|
|
||||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
|
||||||
var grouped_buttons: Dictionary = {}
|
|
||||||
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
|
||||||
var selected_tabs: Array = []
|
|
||||||
var selected_group_y: float = INF
|
|
||||||
for group_value: Variant in grouped_buttons.keys():
|
|
||||||
var buttons := grouped_buttons[group_value] as Array
|
|
||||||
if buttons.size() < 2:
|
|
||||||
continue
|
|
||||||
var group_y: float = INF
|
|
||||||
for item: Variant in buttons:
|
|
||||||
var button := item as BaseButton
|
|
||||||
group_y = minf(group_y, button.global_position.y)
|
|
||||||
if group_y < selected_group_y:
|
|
||||||
selected_tabs = buttons
|
|
||||||
selected_group_y = group_y
|
|
||||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
|
||||||
var displaced_focus: bool = selected_tabs.has(focus_owner)
|
|
||||||
for item: Variant in selected_tabs:
|
|
||||||
var tab := item as Control
|
|
||||||
tab.focus_mode = Control.FOCUS_NONE
|
|
||||||
if displaced_focus:
|
|
||||||
focus_owner.release_focus()
|
|
||||||
call_deferred("_focus_current_section")
|
|
||||||
var inventory_tabs: Array[Button] = [
|
var inventory_tabs: Array[Button] = [
|
||||||
_cooler_sub_tab,
|
_cooler_sub_tab,
|
||||||
_tackle_sub_tab,
|
_tackle_sub_tab,
|
||||||
|
|
@ -1953,7 +1359,6 @@ func _refresh_tackle_box() -> void:
|
||||||
row.text = "%s ×%d" % [item.display_name, owned.quantity]
|
row.text = "%s ×%d" % [item.display_name, owned.quantity]
|
||||||
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
row.toggle_mode = true
|
row.toggle_mode = true
|
||||||
row.set_meta(&"controller_tackle_item_id", owned.item_id)
|
|
||||||
row.button_pressed = owned.item_id == _selected_tackle_item_id
|
row.button_pressed = owned.item_id == _selected_tackle_item_id
|
||||||
if item.is_bait() and item.icon != null:
|
if item.is_bait() and item.icon != null:
|
||||||
_apply_tackle_bait_button_style(row)
|
_apply_tackle_bait_button_style(row)
|
||||||
|
|
|
||||||
|
|
@ -336,32 +336,7 @@ func _confirm(text: String, action: Callable) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _focus_first() -> void:
|
func _focus_first() -> void:
|
||||||
var candidates: Array[Control] = []
|
for child: Node in _tabs.get_children():
|
||||||
_collect_focusable_player_controls(self, candidates)
|
if child is Button and child.visible and not child.disabled:
|
||||||
if candidates.is_empty():
|
child.grab_focus()
|
||||||
return
|
return
|
||||||
candidates.sort_custom(func(first: Control, second: Control) -> bool:
|
|
||||||
if not is_equal_approx(first.global_position.y, second.global_position.y):
|
|
||||||
return first.global_position.y < second.global_position.y
|
|
||||||
return first.global_position.x < second.global_position.x
|
|
||||||
)
|
|
||||||
candidates[0].grab_focus()
|
|
||||||
|
|
||||||
|
|
||||||
func _collect_focusable_player_controls(
|
|
||||||
root: Node,
|
|
||||||
output: Array[Control],
|
|
||||||
) -> void:
|
|
||||||
for child: Node in root.get_children():
|
|
||||||
if child == _tabs:
|
|
||||||
continue
|
|
||||||
var control := child as Control
|
|
||||||
if control != null and not control.is_visible_in_tree():
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
control != null
|
|
||||||
and control.focus_mode != Control.FOCUS_NONE
|
|
||||||
and not control is ScrollBar
|
|
||||||
):
|
|
||||||
output.append(control)
|
|
||||||
_collect_focusable_player_controls(child, output)
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
class_name SettingsBubblePage
|
class_name SettingsBubblePage
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
const ControllerFocusNavigationType = preload(
|
|
||||||
"res://ui/controller_focus_navigation.gd"
|
|
||||||
)
|
|
||||||
|
|
||||||
@export var page_id: StringName
|
@export var page_id: StringName
|
||||||
@export var cluster_path: NodePath = ^"BubbleCluster"
|
@export var cluster_path: NodePath = ^"BubbleCluster"
|
||||||
@export var bubble_paths: Array[NodePath] = []
|
@export var bubble_paths: Array[NodePath] = []
|
||||||
|
|
@ -173,13 +169,23 @@ func _update_layout() -> void:
|
||||||
_cluster.position = _resting_cluster_position
|
_cluster.position = _resting_cluster_position
|
||||||
_cluster.size = field_size
|
_cluster.size = field_size
|
||||||
_cluster.apply_layout(field_size, compact)
|
_cluster.apply_layout(field_size, compact)
|
||||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
|
||||||
|
|
||||||
|
|
||||||
func _configure_focus_order() -> void:
|
func _configure_focus_order() -> void:
|
||||||
for bubble: BubbleButton in _bubbles:
|
for bubble: BubbleButton in _bubbles:
|
||||||
bubble.focus_mode = Control.FOCUS_NONE
|
bubble.focus_mode = Control.FOCUS_NONE
|
||||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
for index: int in _focus_bubbles.size():
|
||||||
|
var bubble: BubbleButton = _focus_bubbles[index]
|
||||||
|
if index > 0:
|
||||||
|
bubble.focus_neighbor_top = bubble.get_path_to(
|
||||||
|
_focus_bubbles[index - 1]
|
||||||
|
)
|
||||||
|
bubble.focus_neighbor_left = bubble.focus_neighbor_top
|
||||||
|
if index + 1 < _focus_bubbles.size():
|
||||||
|
bubble.focus_neighbor_bottom = bubble.get_path_to(
|
||||||
|
_focus_bubbles[index + 1]
|
||||||
|
)
|
||||||
|
bubble.focus_neighbor_right = bubble.focus_neighbor_bottom
|
||||||
|
|
||||||
|
|
||||||
func _set_interactive(interactive: bool) -> void:
|
func _set_interactive(interactive: bool) -> void:
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ enum PresentationMode {
|
||||||
@onready var _mouse_value: BubbleButton = %MouseValue
|
@onready var _mouse_value: BubbleButton = %MouseValue
|
||||||
@onready var _controller_value: BubbleButton = %ControllerValue
|
@onready var _controller_value: BubbleButton = %ControllerValue
|
||||||
@onready var _invert_y_toggle: BubbleButton = %InvertYToggle
|
@onready var _invert_y_toggle: BubbleButton = %InvertYToggle
|
||||||
@onready var _on_screen_keyboard_toggle: BubbleButton = %OnScreenKeyboardToggle
|
|
||||||
@onready var _auto_click_toggle: BubbleButton = %AutoClickToggle
|
@onready var _auto_click_toggle: BubbleButton = %AutoClickToggle
|
||||||
@onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue
|
@onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue
|
||||||
|
|
||||||
|
|
@ -85,7 +84,6 @@ var _auto_click_interval_value: float = 0.20
|
||||||
var _mouse_sensitivity: float = 0.005
|
var _mouse_sensitivity: float = 0.005
|
||||||
var _controller_sensitivity: float = 2.5
|
var _controller_sensitivity: float = 2.5
|
||||||
var _invert_camera_y: bool = false
|
var _invert_camera_y: bool = false
|
||||||
var _on_screen_keyboard_enabled: bool = false
|
|
||||||
var _network_profile: NetworkProfilePreferences
|
var _network_profile: NetworkProfilePreferences
|
||||||
var _network_session: NetworkSession
|
var _network_session: NetworkSession
|
||||||
var _data_root: PlayerDataRoot
|
var _data_root: PlayerDataRoot
|
||||||
|
|
@ -163,7 +161,6 @@ func _ready() -> void:
|
||||||
_adjust_controller_sensitivity.bind(1)
|
_adjust_controller_sensitivity.bind(1)
|
||||||
)
|
)
|
||||||
_invert_y_toggle.pressed.connect(_toggle_invert_y)
|
_invert_y_toggle.pressed.connect(_toggle_invert_y)
|
||||||
_on_screen_keyboard_toggle.pressed.connect(_toggle_on_screen_keyboard)
|
|
||||||
_auto_click_toggle.pressed.connect(_toggle_auto_click)
|
_auto_click_toggle.pressed.connect(_toggle_auto_click)
|
||||||
for control: Control in [_auto_click_toggle, _auto_click_interval]:
|
for control: Control in [_auto_click_toggle, _auto_click_interval]:
|
||||||
control.mouse_entered.connect(
|
control.mouse_entered.connect(
|
||||||
|
|
@ -707,7 +704,6 @@ func _apply_settings() -> void:
|
||||||
edited.mouse_camera_sensitivity = _mouse_sensitivity
|
edited.mouse_camera_sensitivity = _mouse_sensitivity
|
||||||
edited.controller_camera_sensitivity = _controller_sensitivity
|
edited.controller_camera_sensitivity = _controller_sensitivity
|
||||||
edited.invert_camera_y = _invert_camera_y
|
edited.invert_camera_y = _invert_camera_y
|
||||||
edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled
|
|
||||||
if _settings_manager.apply_settings(edited):
|
if _settings_manager.apply_settings(edited):
|
||||||
if (
|
if (
|
||||||
_presentation_mode == PresentationMode.TITLE_EMBEDDED
|
_presentation_mode == PresentationMode.TITLE_EMBEDDED
|
||||||
|
|
@ -729,7 +725,6 @@ func _load_controls() -> void:
|
||||||
_mouse_sensitivity = settings.mouse_camera_sensitivity
|
_mouse_sensitivity = settings.mouse_camera_sensitivity
|
||||||
_controller_sensitivity = settings.controller_camera_sensitivity
|
_controller_sensitivity = settings.controller_camera_sensitivity
|
||||||
_invert_camera_y = settings.invert_camera_y
|
_invert_camera_y = settings.invert_camera_y
|
||||||
_on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled
|
|
||||||
_refresh_value_labels()
|
_refresh_value_labels()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -760,10 +755,6 @@ func _refresh_value_labels() -> void:
|
||||||
"invert\nvertical\ncamera\n"
|
"invert\nvertical\ncamera\n"
|
||||||
+ ("on" if _invert_camera_y else "off")
|
+ ("on" if _invert_camera_y else "off")
|
||||||
)
|
)
|
||||||
_on_screen_keyboard_toggle.text = (
|
|
||||||
"on-screen\nkeyboard\n"
|
|
||||||
+ ("on" if _on_screen_keyboard_enabled else "off")
|
|
||||||
)
|
|
||||||
_auto_click_toggle.text = (
|
_auto_click_toggle.text = (
|
||||||
"accessibility\nauto-click\n"
|
"accessibility\nauto-click\n"
|
||||||
+ ("on" if _auto_click_enabled else "off")
|
+ ("on" if _auto_click_enabled else "off")
|
||||||
|
|
@ -892,11 +883,6 @@ func _toggle_invert_y() -> void:
|
||||||
_refresh_value_labels()
|
_refresh_value_labels()
|
||||||
|
|
||||||
|
|
||||||
func _toggle_on_screen_keyboard() -> void:
|
|
||||||
_on_screen_keyboard_enabled = not _on_screen_keyboard_enabled
|
|
||||||
_refresh_value_labels()
|
|
||||||
|
|
||||||
|
|
||||||
func _toggle_auto_click() -> void:
|
func _toggle_auto_click() -> void:
|
||||||
_auto_click_enabled = not _auto_click_enabled
|
_auto_click_enabled = not _auto_click_enabled
|
||||||
_refresh_value_labels()
|
_refresh_value_labels()
|
||||||
|
|
|
||||||
|
|
@ -397,8 +397,8 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("3_page")
|
script = ExtResource("3_page")
|
||||||
page_id = &"controls"
|
page_id = &"controls"
|
||||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||||
|
|
||||||
|
|
@ -491,29 +491,17 @@ neutral_size = Vector2(176, 170)
|
||||||
compact_minimum_size = Vector2(170, 164)
|
compact_minimum_size = Vector2(170, 164)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 24
|
maximum_font_size = 24
|
||||||
desktop_anchor = Vector2(525, 105)
|
desktop_anchor = Vector2(525, 145)
|
||||||
compact_anchor = Vector2(485, 95)
|
compact_anchor = Vector2(485, 135)
|
||||||
motion_phase = 4.5
|
motion_phase = 4.5
|
||||||
|
|
||||||
[node name="OnScreenKeyboardToggle" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
|
||||||
unique_name_in_owner = true
|
|
||||||
custom_minimum_size = Vector2(0, 0)
|
|
||||||
text = "on-screen\nkeyboard\noff"
|
|
||||||
neutral_size = Vector2(170, 150)
|
|
||||||
compact_minimum_size = Vector2(154, 136)
|
|
||||||
minimum_font_size = 14
|
|
||||||
maximum_font_size = 22
|
|
||||||
desktop_anchor = Vector2(525, 255)
|
|
||||||
compact_anchor = Vector2(485, 235)
|
|
||||||
motion_phase = 4.7
|
|
||||||
|
|
||||||
[node name="ControllerMapping" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
[node name="ControllerMapping" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "map\ncontroller"
|
text = "map\ncontroller"
|
||||||
neutral_size = Vector2(160, 150)
|
neutral_size = Vector2(160, 150)
|
||||||
desktop_anchor = Vector2(525, 390)
|
desktop_anchor = Vector2(525, 330)
|
||||||
compact_anchor = Vector2(485, 355)
|
compact_anchor = Vector2(485, 300)
|
||||||
compact_minimum_size = Vector2(148, 138)
|
compact_minimum_size = Vector2(148, 138)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 23
|
maximum_font_size = 23
|
||||||
|
|
@ -524,8 +512,8 @@ unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "back"
|
text = "back"
|
||||||
neutral_size = Vector2(100, 94)
|
neutral_size = Vector2(100, 94)
|
||||||
desktop_anchor = Vector2(635, 470)
|
desktop_anchor = Vector2(635, 445)
|
||||||
compact_anchor = Vector2(555, 420)
|
compact_anchor = Vector2(555, 405)
|
||||||
compact_minimum_size = Vector2(84, 80)
|
compact_minimum_size = Vector2(84, 80)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 17
|
maximum_font_size = 17
|
||||||
|
|
|
||||||
|
|
@ -608,10 +608,6 @@ func _prepare_awaiting_start_input() -> void:
|
||||||
_navigation_focus_active = false
|
_navigation_focus_active = false
|
||||||
_modal_restore_navigation_focus = false
|
_modal_restore_navigation_focus = false
|
||||||
_button_center.show()
|
_button_center.show()
|
||||||
var bubble_field := get_node_or_null("%BubbleField") as Control
|
|
||||||
if bubble_field != null:
|
|
||||||
bubble_field.hide()
|
|
||||||
call_deferred("set_process", visible)
|
|
||||||
_feedback_label.show()
|
_feedback_label.show()
|
||||||
_feedback_label.modulate.a = 0.0
|
_feedback_label.modulate.a = 0.0
|
||||||
_continue_stats_hovered = false
|
_continue_stats_hovered = false
|
||||||
|
|
@ -654,10 +650,6 @@ func _reveal_primary_menu() -> void:
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
_awaiting_start_input = false
|
_awaiting_start_input = false
|
||||||
_button_center.show()
|
|
||||||
var bubble_field := get_node_or_null("%BubbleField") as Control
|
|
||||||
if bubble_field != null:
|
|
||||||
bubble_field.show()
|
|
||||||
_stop_entry_prompt_animation()
|
_stop_entry_prompt_animation()
|
||||||
_title_entry_generation += 1
|
_title_entry_generation += 1
|
||||||
_title_entry_transition_active = true
|
_title_entry_transition_active = true
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,6 @@ const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(256, 180)
|
||||||
const UIReferencePresentationType = preload(
|
const UIReferencePresentationType = preload(
|
||||||
"res://ui/ui_reference_presentation.gd"
|
"res://ui/ui_reference_presentation.gd"
|
||||||
)
|
)
|
||||||
const ControllerFocusPresentationType = preload(
|
|
||||||
"res://ui/controller_focus_presentation.gd"
|
|
||||||
)
|
|
||||||
const ControllerFocusRecoveryType = preload(
|
|
||||||
"res://ui/controller_focus_recovery.gd"
|
|
||||||
)
|
|
||||||
const OnScreenKeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
|
||||||
|
|
||||||
signal effective_pixel_size_changed(
|
signal effective_pixel_size_changed(
|
||||||
requested_pixel_size: int,
|
requested_pixel_size: int,
|
||||||
|
|
@ -23,9 +16,6 @@ signal effective_pixel_size_changed(
|
||||||
@onready var _canonical_stage: Control = (
|
@onready var _canonical_stage: Control = (
|
||||||
$UIViewport/GameUI/UIRoot/CanonicalStage
|
$UIViewport/GameUI/UIRoot/CanonicalStage
|
||||||
)
|
)
|
||||||
@onready var _hotbar: HotbarUI = (
|
|
||||||
$UIViewport/GameUI/UIRoot/CanonicalStage/Hotbar
|
|
||||||
)
|
|
||||||
@onready var _chat_ui: ChatUI = $UIViewport/GameUI/UIRoot/ChatUI
|
@onready var _chat_ui: ChatUI = $UIViewport/GameUI/UIRoot/ChatUI
|
||||||
@onready var _title_content_stage: Control = (
|
@onready var _title_content_stage: Control = (
|
||||||
$UIViewport/GameUI/UIRoot/TitleScreen/ResponsiveTitleStage
|
$UIViewport/GameUI/UIRoot/TitleScreen/ResponsiveTitleStage
|
||||||
|
|
@ -36,25 +26,14 @@ var _effective_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE
|
||||||
var _gameplay_active: bool = false
|
var _gameplay_active: bool = false
|
||||||
var _interactive_ui_open: bool = false
|
var _interactive_ui_open: bool = false
|
||||||
var _passive_pointer_ui_enabled: bool = false
|
var _passive_pointer_ui_enabled: bool = false
|
||||||
var _on_screen_keyboard: OnScreenKeyboardType
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_on_screen_keyboard = OnScreenKeyboardType.new()
|
|
||||||
_ui_root.add_child(_on_screen_keyboard)
|
|
||||||
var controller_focus_recovery := ControllerFocusRecoveryType.new()
|
|
||||||
_ui_root.add_child(controller_focus_recovery)
|
|
||||||
var controller_focus_presentation := ControllerFocusPresentationType.new()
|
|
||||||
_ui_root.add_child(controller_focus_presentation)
|
|
||||||
var root_viewport: Viewport = get_viewport()
|
var root_viewport: Viewport = get_viewport()
|
||||||
root_viewport.size_changed.connect(_resize_presentation)
|
root_viewport.size_changed.connect(_resize_presentation)
|
||||||
_resize_presentation()
|
_resize_presentation()
|
||||||
|
|
||||||
|
|
||||||
func set_on_screen_keyboard_enabled(enabled: bool) -> void:
|
|
||||||
_on_screen_keyboard.set_enabled(enabled)
|
|
||||||
|
|
||||||
|
|
||||||
func set_pixel_size(pixel_size: int) -> void:
|
func set_pixel_size(pixel_size: int) -> void:
|
||||||
_requested_pixel_size = clampi(
|
_requested_pixel_size = clampi(
|
||||||
pixel_size,
|
pixel_size,
|
||||||
|
|
@ -141,7 +120,6 @@ func _resize_presentation() -> void:
|
||||||
UIReferencePresentationType.get_stage_position(display_size)
|
UIReferencePresentationType.get_stage_position(display_size)
|
||||||
)
|
)
|
||||||
_canonical_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
_canonical_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
||||||
_hotbar.position = Vector2(0.0, _canonical_stage.position.y)
|
|
||||||
_title_content_stage.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
_title_content_stage.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||||
_title_content_stage.position = _canonical_stage.position
|
_title_content_stage.position = _canonical_stage.position
|
||||||
_title_content_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
_title_content_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# World authoring
|
# World authoring
|
||||||
|
|
||||||
The active test world uses the canonical starter island through
|
The active test world uses the human-authored starter island through
|
||||||
`world/regions/starter_island_region.tscn`.
|
`world/regions/starter_island_region.tscn`.
|
||||||
|
|
||||||
## Active composition
|
## Active composition
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue