Compare commits
4 commits
cf7a963f18
...
ded8494cc6
| Author | SHA1 | Date | |
|---|---|---|---|
| ded8494cc6 | |||
| 4ac7d5edb8 | |||
| 26c9308fcf | |||
| 6fac52dd96 |
50 changed files with 2881 additions and 130 deletions
|
|
@ -71,7 +71,7 @@ detail.
|
|||
|
||||
## Authorship and assets
|
||||
|
||||
NETfishing is human-directed and uses a mixture of human-authored assets and
|
||||
NETfishing uses a mixture of original assets and
|
||||
software-development assistance. The project records this plainly rather than
|
||||
claiming that repository history can prove how each line was produced. See
|
||||
[`CREDITS.md`](CREDITS.md) and
|
||||
|
|
|
|||
28
docs/PORTMASTER-PERFORMANCE.md
Normal file
28
docs/PORTMASTER-PERFORMANCE.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# 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.
|
||||
137
docs/PORTMASTER-RELEASE.md
Normal file
137
docs/PORTMASTER-RELEASE.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# 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,6 +380,9 @@ func _initialize_after_data_root() -> void:
|
|||
_shop_interaction.local_player_range_changed.connect(
|
||||
_on_shop_range_changed
|
||||
)
|
||||
_game_ui.set_shop_npc_player_in_range(
|
||||
_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
_save_manager.setup(
|
||||
_player.inventory,
|
||||
_player.collection_log,
|
||||
|
|
@ -1099,6 +1102,9 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
):
|
||||
_game_ui.set_shop_prompt_visible(false)
|
||||
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:
|
||||
|
|
@ -1119,6 +1125,9 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
|||
return
|
||||
_apply_world_pixelation(settings.world_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(
|
||||
settings.world_pixel_size
|
||||
)
|
||||
|
|
@ -1141,7 +1150,11 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
|||
func _apply_world_pixelation(pixel_size: int) -> void:
|
||||
var root_viewport: Viewport = get_viewport()
|
||||
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
|
||||
root_viewport.scaling_3d_scale = 1.0
|
||||
root_viewport.scaling_3d_scale = (
|
||||
0.75
|
||||
if OS.get_environment("NETFISHING_LOW_END") == "1"
|
||||
else 1.0
|
||||
)
|
||||
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
|
||||
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
|
||||
root_viewport.use_taa = false
|
||||
|
|
@ -1670,6 +1683,7 @@ func _exit_tree() -> void:
|
|||
|
||||
|
||||
func _on_shop_range_changed(in_range: bool) -> void:
|
||||
_game_ui.set_shop_npc_player_in_range(in_range)
|
||||
if not in_range:
|
||||
_game_ui.get_fishing_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
||||
|
|
|
|||
155
scripts/build_portmaster.sh
Executable file
155
scripts/build_portmaster.sh
Executable file
|
|
@ -0,0 +1,155 @@
|
|||
#!/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}"
|
||||
120
scripts/portmaster/NETfishing.sh
Executable file
120
scripts/portmaster/NETfishing.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
#!/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
|
||||
13
scripts/portmaster/gameinfo.xml
Executable file
13
scripts/portmaster/gameinfo.xml
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
<?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>
|
||||
50
scripts/portmaster/licenses/CREDITS.md
Executable file
50
scripts/portmaster/licenses/CREDITS.md
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
# 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).
|
||||
11
scripts/portmaster/licenses/Tuffy-LICENSE.txt
Executable file
11
scripts/portmaster/licenses/Tuffy-LICENSE.txt
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
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
|
||||
39
scripts/portmaster/port.json
Normal file
39
scripts/portmaster/port.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
BIN
scripts/portmaster/screenshot.png
Executable file
BIN
scripts/portmaster/screenshot.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
40
scripts/portmaster/screenshot.png.import
Normal file
40
scripts/portmaster/screenshot.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[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_RELEASE_THRESHOLD: float = 0.30
|
||||
const ACTIVE_DEVICE_AXIS_THRESHOLD: float = 0.35
|
||||
const MUOS_MAPPING_REVISION: String = "muos-v3"
|
||||
const MUOS_MAPPING_REVISION: String = "muos-v4"
|
||||
const MUOS_CONTROLLER_NAMES: Array[String] = [
|
||||
"muOS-Keys",
|
||||
"Deeplay-keys",
|
||||
]
|
||||
const MUOS_MAPPING_BINDINGS: String = (
|
||||
"a:b4,b:b3,x:b5,y:b6,"
|
||||
+ "leftshoulder:b7,rightshoulder:b8,"
|
||||
+ "lefttrigger:b13,righttrigger:b14,"
|
||||
+ "guide:b11,start:b10,back:b9,"
|
||||
"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:b12,"
|
||||
+ "rightx:a2,righty:a3,rightstick:b15,platform:Linux,"
|
||||
+ "leftx:a0,lefty:a1,leftstick:b9,"
|
||||
+ "rightx:a2,righty:a3,rightstick:b12,platform:Linux,"
|
||||
)
|
||||
const ROLE_A: StringName = &"a"
|
||||
const ROLE_B: StringName = &"b"
|
||||
|
|
@ -739,8 +739,8 @@ static func should_install_muos_compatibility_mapping(
|
|||
controller_name: String,
|
||||
is_known: bool,
|
||||
) -> bool:
|
||||
# Current SDL databases should remain authoritative. This fallback exists
|
||||
# only for legacy muOS virtual controllers that SDL cannot identify.
|
||||
# Never update an already-recognized virtual controller at runtime. Godot
|
||||
# can stop delivering its standardized input events after that replacement.
|
||||
return is_muos_controller_name(controller_name) and not is_known
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ 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.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
|
||||
@export var invert_camera_y: bool = false
|
||||
@export var on_screen_keyboard_enabled: bool = false
|
||||
@export var chat_draft: String = ""
|
||||
@export var chat_collapsed: bool = false
|
||||
@export var chat_dock_right: bool = false
|
||||
|
|
@ -63,6 +64,7 @@ func copy() -> PlayerSettings:
|
|||
result.mouse_camera_sensitivity = mouse_camera_sensitivity
|
||||
result.controller_camera_sensitivity = controller_camera_sensitivity
|
||||
result.invert_camera_y = invert_camera_y
|
||||
result.on_screen_keyboard_enabled = on_screen_keyboard_enabled
|
||||
result.chat_draft = chat_draft
|
||||
result.chat_collapsed = chat_collapsed
|
||||
result.chat_dock_right = chat_dock_right
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ func load_settings() -> bool:
|
|||
if (
|
||||
typeof(accessibility.get("auto_click_enabled")) != 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 (
|
||||
accessibility.has("use_readable_interface_font")
|
||||
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
|
||||
|
|
@ -88,6 +92,9 @@ func load_settings() -> bool:
|
|||
-1.0
|
||||
)
|
||||
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(
|
||||
presentation.get(
|
||||
"world_pixel_size",
|
||||
|
|
@ -176,6 +183,9 @@ func save_now() -> bool:
|
|||
"auto_click_enabled": current_settings.auto_click_enabled,
|
||||
"auto_click_interval": current_settings.auto_click_interval,
|
||||
"use_readable_interface_font": true,
|
||||
"on_screen_keyboard_enabled": (
|
||||
current_settings.on_screen_keyboard_enabled
|
||||
),
|
||||
},
|
||||
"camera": {
|
||||
"mouse_sensitivity": current_settings.mouse_camera_sensitivity,
|
||||
|
|
|
|||
75
tests/controller_focus_presentation_validation.gd
Normal file
75
tests/controller_focus_presentation_validation.gd
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
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
tests/controller_focus_presentation_validation.gd.uid
Normal file
1
tests/controller_focus_presentation_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bjay8ki3albk6
|
||||
88
tests/controller_focus_recovery_validation.gd
Normal file
88
tests/controller_focus_recovery_validation.gd
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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
tests/controller_focus_recovery_validation.gd.uid
Normal file
1
tests/controller_focus_recovery_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cqk75fxykqrhs
|
||||
|
|
@ -18,6 +18,8 @@ func _run() -> void:
|
|||
root.add_child(manager)
|
||||
await process_frame
|
||||
var failure: String = _validate_manager(manager)
|
||||
if failure.is_empty():
|
||||
failure = _validate_portmaster_launcher()
|
||||
if failure.is_empty():
|
||||
failure = _validate_auto_map(manager)
|
||||
if failure.is_empty():
|
||||
|
|
@ -40,19 +42,19 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
|||
)
|
||||
)
|
||||
for expected_binding: String in [
|
||||
"a:b4",
|
||||
"b:b3",
|
||||
"x:b5",
|
||||
"y:b6",
|
||||
"leftshoulder:b7",
|
||||
"rightshoulder:b8",
|
||||
"lefttrigger:b13",
|
||||
"righttrigger:b14",
|
||||
"back:b9",
|
||||
"start:b10",
|
||||
"guide:b11",
|
||||
"leftstick:b12",
|
||||
"rightstick:b15",
|
||||
"a:b0",
|
||||
"b:b1",
|
||||
"x:b3",
|
||||
"y:b2",
|
||||
"leftshoulder:b4",
|
||||
"rightshoulder:b5",
|
||||
"lefttrigger:b10",
|
||||
"righttrigger:b11",
|
||||
"back:b6",
|
||||
"start:b7",
|
||||
"guide:b8",
|
||||
"leftstick:b9",
|
||||
"rightstick:b12",
|
||||
"leftx:a0",
|
||||
"righty:a3",
|
||||
]:
|
||||
|
|
@ -79,7 +81,7 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
|||
"muOS-Keys",
|
||||
true,
|
||||
):
|
||||
return "recognized muOS controller would override its SDL mapping"
|
||||
return "recognized muOS controller would be replaced at runtime"
|
||||
if ControllerMappingManagerType.should_install_muos_compatibility_mapping(
|
||||
"ordinary controller",
|
||||
false,
|
||||
|
|
@ -194,6 +196,30 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
|||
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:
|
||||
var panel := ControllerMappingPanelType.new()
|
||||
root.add_child(panel)
|
||||
|
|
@ -258,13 +284,35 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
|||
return "manual remapping still dictates a specific physical input"
|
||||
if "any button" not in panel._progress_label.text.to_lower():
|
||||
return "manual remapping does not request a generic controller input"
|
||||
var cancel_button := InputEventJoypadButton.new()
|
||||
cancel_button.device = manager.get_active_device_id()
|
||||
cancel_button.button_index = JOY_BUTTON_B
|
||||
cancel_button.pressed = true
|
||||
manager.controller_input_observed.emit(cancel_button)
|
||||
if panel.is_capturing():
|
||||
return "mapped controller back input did not cancel capture"
|
||||
panel._cancel_capture()
|
||||
panel._begin_auto_map()
|
||||
panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||
var lone_button := InputEventJoypadButton.new()
|
||||
lone_button.device = manager.get_active_device_id()
|
||||
lone_button.button_index = JOY_BUTTON_B
|
||||
lone_button.pressed = true
|
||||
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._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
|
||||
var held_left := InputEventJoypadMotion.new()
|
||||
|
|
|
|||
82
tests/controller_ui_navigation_validation.gd
Normal file
82
tests/controller_ui_navigation_validation.gd
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
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
tests/controller_ui_navigation_validation.gd.uid
Normal file
1
tests/controller_ui_navigation_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cbv0ia42rqu41
|
||||
82
tests/on_screen_keyboard_validation.gd
Normal file
82
tests/on_screen_keyboard_validation.gd
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
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
tests/on_screen_keyboard_validation.gd.uid
Normal file
1
tests/on_screen_keyboard_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bgmt6joqdk37q
|
||||
|
|
@ -94,7 +94,10 @@ func _run() -> void:
|
|||
assert(player_menu.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
assert(hotbar.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(hotbar.position.is_equal_approx(Vector2(
|
||||
0.0,
|
||||
canonical_stage.position.y,
|
||||
)))
|
||||
assert(hotbar.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
|
|
|
|||
|
|
@ -568,6 +568,21 @@ func _refresh_input_ownership() -> void:
|
|||
)
|
||||
_panel.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:
|
||||
|
|
@ -886,9 +901,14 @@ func _refresh_visibility() -> void:
|
|||
_collapse_button.show()
|
||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||
_panel.show()
|
||||
_collapse_button.focus_mode = (
|
||||
Control.FOCUS_ALL if _opened else Control.FOCUS_NONE
|
||||
)
|
||||
_height_button.visible = not _mobile_mode
|
||||
_height_button.focus_mode = (
|
||||
Control.FOCUS_NONE if _mobile_mode else Control.FOCUS_ALL
|
||||
Control.FOCUS_ALL
|
||||
if _opened and not _mobile_mode
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
_unread_indicator.visible = collapsed and _collapsed_has_unread
|
||||
_hint.visible = not _opened
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ var _base_position: Vector2 = Vector2.ZERO
|
|||
var _presented_size: Vector2 = Vector2.ZERO
|
||||
var _compact: bool = false
|
||||
var _presentation_initialized: bool = false
|
||||
var _controller_preview_active: bool = false
|
||||
var _controller_preview_texture: Texture2D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -146,6 +148,15 @@ func set_drag_enabled(enabled: bool) -> void:
|
|||
_hovered = false
|
||||
|
||||
|
||||
func set_controller_placement_preview(
|
||||
active: bool,
|
||||
texture: Texture2D,
|
||||
) -> void:
|
||||
_controller_preview_active = active
|
||||
_controller_preview_texture = texture
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
if _hotbar == null:
|
||||
return
|
||||
|
|
@ -161,11 +172,16 @@ func refresh() -> void:
|
|||
if _fish_inventory != null and not catch_id.is_empty()
|
||||
else null
|
||||
)
|
||||
_item_icon.texture = (
|
||||
var assigned_texture: Texture2D = (
|
||||
fish_catch.fish.display_texture
|
||||
if fish_catch != 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 = (
|
||||
_bag.get_quantity(item_id)
|
||||
if _bag != null and not item_id.is_empty()
|
||||
|
|
@ -177,7 +193,9 @@ func refresh() -> void:
|
|||
else ""
|
||||
)
|
||||
_quantity_label.text = quantity_text
|
||||
_quantity_label.visible = not quantity_text.is_empty()
|
||||
_quantity_label.visible = (
|
||||
not _controller_preview_active and not quantity_text.is_empty()
|
||||
)
|
||||
tooltip_text = (
|
||||
"%s · %.1f lb" % [
|
||||
FishQualityType.qualified_name(
|
||||
|
|
@ -192,7 +210,11 @@ func refresh() -> void:
|
|||
var was_selected: bool = _selected
|
||||
var was_empty: bool = _empty
|
||||
_selected = slot_index == _hotbar.get_selected_slot()
|
||||
_empty = item == null and fish_catch == null
|
||||
_empty = (
|
||||
not _controller_preview_active
|
||||
and item == null
|
||||
and fish_catch == null
|
||||
)
|
||||
if was_selected != _selected or was_empty != _empty:
|
||||
_apply_style()
|
||||
|
||||
|
|
@ -220,7 +242,9 @@ func _apply_style() -> void:
|
|||
var normal_fill: Color = profile.normal_fill
|
||||
normal_fill.a = 1.0
|
||||
var selected_fill: Color = normal_fill
|
||||
if _selected:
|
||||
if _controller_preview_active:
|
||||
selected_fill = normal_fill.lightened(0.22)
|
||||
elif _selected:
|
||||
selected_fill = normal_fill.lightened(0.12)
|
||||
add_theme_stylebox_override(
|
||||
"normal",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,53 @@ func _ready() -> void:
|
|||
_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(
|
||||
center: Vector2,
|
||||
bubble_size: Vector2,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
class_name BubbleCluster
|
||||
extends Control
|
||||
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
|
||||
@export var profile: BubbleMenuProfile
|
||||
@export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0)
|
||||
@export var compact_reference_size: Vector2 = Vector2(294.0, 200.0)
|
||||
|
|
@ -18,14 +22,6 @@ func configure(bubbles: Array[BubbleButton]) -> void:
|
|||
if bubble.profile == null:
|
||||
bubble.profile = 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:
|
||||
|
|
@ -52,6 +48,7 @@ func apply_layout(field_size: Vector2, compact: bool) -> void:
|
|||
bubble_size,
|
||||
profile.font_size_ratio
|
||||
)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_bubbles)
|
||||
|
||||
|
||||
func advance_motion(delta: float) -> void:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ var _quality_color := Color.WHITE
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
set_meta(&"controller_focus_inversion_disabled", true)
|
||||
resized.connect(_update_visual_pivot)
|
||||
focus_entered.connect(_refresh_style)
|
||||
focus_exited.connect(_refresh_style)
|
||||
|
|
|
|||
57
ui/controller_focus_navigation.gd
Normal file
57
ui/controller_focus_navigation.gd
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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
ui/controller_focus_navigation.gd.uid
Normal file
1
ui/controller_focus_navigation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dfb6bobetmujn
|
||||
97
ui/controller_focus_presentation.gd
Normal file
97
ui/controller_focus_presentation.gd
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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
ui/controller_focus_presentation.gd.uid
Normal file
1
ui/controller_focus_presentation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b2ccvcx1rfd1x
|
||||
142
ui/controller_focus_recovery.gd
Normal file
142
ui/controller_focus_recovery.gd
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
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
ui/controller_focus_recovery.gd.uid
Normal file
1
ui/controller_focus_recovery.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c7cw61ethr3pt
|
||||
|
|
@ -8,6 +8,7 @@ const ControllerMappingManagerType = preload(
|
|||
)
|
||||
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||
const CAPTURE_NEUTRAL_SECONDS: float = 0.22
|
||||
const CANCEL_COMBO_HOLD_SECONDS: float = 1.25
|
||||
|
||||
var _mapping_manager: ControllerMappingManagerType
|
||||
var _binding_buttons: Dictionary = {}
|
||||
|
|
@ -24,6 +25,9 @@ var _auto_map_draft: Dictionary = {}
|
|||
var _capture_device_id: int = 0
|
||||
var _waiting_for_neutral: bool = false
|
||||
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:
|
||||
|
|
@ -112,6 +116,14 @@ func _process(delta: float) -> void:
|
|||
or _capturing_role.is_empty()
|
||||
):
|
||||
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 not _mapping_manager.are_capture_inputs_neutral(_capture_device_id):
|
||||
_neutral_elapsed = 0.0
|
||||
|
|
@ -152,18 +164,7 @@ func _on_controller_input_observed(event: InputEvent) -> void:
|
|||
return
|
||||
if event_device != _capture_device_id:
|
||||
return
|
||||
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
|
||||
_track_cancel_combo_button(button_event)
|
||||
if _waiting_for_neutral:
|
||||
return
|
||||
_try_capture_event(event)
|
||||
|
|
@ -332,6 +333,7 @@ func _begin_auto_map() -> void:
|
|||
_auto_map_active = true
|
||||
_auto_map_index = 0
|
||||
_auto_map_draft = {}
|
||||
_reset_cancel_combo()
|
||||
_capture_device_id = _mapping_manager.get_active_device_id()
|
||||
_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])
|
||||
|
||||
|
|
@ -340,6 +342,7 @@ func _begin_manual_capture(role: StringName) -> void:
|
|||
_auto_map_active = false
|
||||
_auto_map_index = -1
|
||||
_auto_map_draft.clear()
|
||||
_reset_cancel_combo()
|
||||
_capture_device_id = _mapping_manager.get_active_device_id()
|
||||
_set_capture_role(role)
|
||||
|
||||
|
|
@ -356,10 +359,14 @@ func _refresh_capture_prompt() -> void:
|
|||
if _capturing_role.is_empty():
|
||||
return
|
||||
if _auto_map_active:
|
||||
_progress_label.text = "step %d of %d: %s" % [
|
||||
_progress_label.text = (
|
||||
"step %d of %d: %s\n"
|
||||
+ "hold both bumpers for %.2f seconds to cancel"
|
||||
) % [
|
||||
_auto_map_index + 1,
|
||||
ControllerMappingManagerType.ROLE_ORDER.size(),
|
||||
_mapping_manager.get_role_prompt(_capturing_role),
|
||||
CANCEL_COMBO_HOLD_SECONDS,
|
||||
]
|
||||
return
|
||||
var input_instruction: String = "press any controller button"
|
||||
|
|
@ -408,11 +415,34 @@ func _cancel_capture() -> void:
|
|||
_auto_map_draft.clear()
|
||||
_waiting_for_neutral = false
|
||||
_neutral_elapsed = 0.0
|
||||
_reset_cancel_combo()
|
||||
_set_action_buttons_disabled(false)
|
||||
if _progress_label != null:
|
||||
_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:
|
||||
if _auto_map_button != null:
|
||||
_auto_map_button.disabled = disabled
|
||||
|
|
|
|||
106
ui/game_ui.gd
106
ui/game_ui.gd
|
|
@ -133,6 +133,8 @@ const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
|
|||
@onready var _shop_prompt_key: Label = %ShopPromptKey
|
||||
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
|
||||
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 _chat_ui: ChatUIType = %ChatUI
|
||||
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
|
||||
|
|
@ -207,6 +209,18 @@ func _ready() -> void:
|
|||
_hotbar_ui.presentation_transition_finished.connect(
|
||||
_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(
|
||||
_on_settings_visibility_changed
|
||||
)
|
||||
|
|
@ -503,7 +517,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
|
|||
_chat_ui.toggle_chat()
|
||||
return true
|
||||
if focus_pressed:
|
||||
_chat_ui.toggle_focus()
|
||||
_chat_ui.refocus_gameplay()
|
||||
return true
|
||||
if accept_pressed:
|
||||
return _chat_ui.request_virtual_keyboard()
|
||||
|
|
@ -1302,6 +1316,8 @@ func _on_hud_bait_inventory_changed() -> void:
|
|||
|
||||
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||
_gameplay_ui_enabled = enabled
|
||||
if enabled:
|
||||
_try_start_shop_npc_speech()
|
||||
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
||||
_experience_presentation.visible = enabled
|
||||
_refresh_chat_availability()
|
||||
|
|
@ -1362,7 +1378,6 @@ func set_shop_prompt_visible(
|
|||
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
||||
) -> void:
|
||||
_apply_shop_prompt_style()
|
||||
var was_visible := _shop_prompt.visible
|
||||
_shop_prompt.visible = (
|
||||
is_visible
|
||||
and _gameplay_ui_enabled
|
||||
|
|
@ -1371,23 +1386,42 @@ func set_shop_prompt_visible(
|
|||
and not _shop_open
|
||||
)
|
||||
if _shop_prompt.visible:
|
||||
if not was_visible:
|
||||
_ensure_shop_animalese_voice()
|
||||
TypewriterRevealType.start(
|
||||
_shop_prompt_message,
|
||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||
)
|
||||
_shop_animalese_voice.speak_text(
|
||||
_shop_prompt_message,
|
||||
_shop_prompt_message.text,
|
||||
"shopkeeper",
|
||||
SHOP_ANIMALESE_VOICE_ID,
|
||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||
)
|
||||
if world_anchor.is_finite():
|
||||
_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()
|
||||
TypewriterRevealType.start(
|
||||
_shop_prompt_message,
|
||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||
)
|
||||
_shop_animalese_voice.speak_text(
|
||||
_shop_prompt_message,
|
||||
_shop_prompt_message.text,
|
||||
"shopkeeper",
|
||||
SHOP_ANIMALESE_VOICE_ID,
|
||||
SHOP_SPEECH_CHARACTERS_PER_SECOND,
|
||||
)
|
||||
|
||||
|
||||
func _ensure_shop_animalese_voice() -> void:
|
||||
if _shop_animalese_voice != null:
|
||||
return
|
||||
|
|
@ -2017,6 +2051,48 @@ func _on_inventory_hotbar_context_changed(show_hotbar: bool) -> void:
|
|||
_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(
|
||||
is_visible: bool,
|
||||
) -> void:
|
||||
|
|
|
|||
128
ui/hotbar.gd
128
ui/hotbar.gd
|
|
@ -4,6 +4,7 @@ extends Control
|
|||
signal presentation_transition_finished(is_visible: bool)
|
||||
|
||||
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 PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
|
|
@ -41,6 +42,13 @@ var _item_name_suppressed: bool = false
|
|||
var _motion_elapsed: float = 0.0
|
||||
var _compact_layout: 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_generation: int = 0
|
||||
|
||||
|
|
@ -99,6 +107,109 @@ func set_drag_enabled(enabled: bool) -> void:
|
|||
_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:
|
||||
if _player_menu_context == enabled:
|
||||
return
|
||||
|
|
@ -230,6 +341,9 @@ func _collect_slots() -> void:
|
|||
slot.item_hover_ended.connect(_on_slot_item_hover_ended)
|
||||
slot.item_drag_started.connect(_on_slot_drag_started)
|
||||
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.sort_custom(
|
||||
func(
|
||||
|
|
@ -278,10 +392,24 @@ func _on_selected_slot_changed(
|
|||
_item_id: StringName,
|
||||
) -> void:
|
||||
_refresh()
|
||||
if _controller_placement_active:
|
||||
_refresh_controller_placement_preview()
|
||||
return
|
||||
if _hovered_slot_index < 0:
|
||||
_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(
|
||||
slot_index: int,
|
||||
item_id: StringName,
|
||||
|
|
|
|||
|
|
@ -453,6 +453,7 @@ func _refresh_catalog() -> void:
|
|||
|
||||
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
||||
var entry := Button.new()
|
||||
entry.set_meta(&"controller_focus_inversion_disabled", true)
|
||||
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
||||
entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||
entry.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
|
|
|
|||
456
ui/on_screen_keyboard.gd
Normal file
456
ui/on_screen_keyboard.gd
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
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
ui/on_screen_keyboard.gd.uid
Normal file
1
ui/on_screen_keyboard.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dcanibf8hordh
|
||||
|
|
@ -85,6 +85,14 @@ const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
|
|||
|
||||
signal menu_visibility_changed(is_open: 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 shop_cooler_modal_changed(is_open: bool)
|
||||
|
||||
|
|
@ -124,6 +132,13 @@ enum CloseReason {
|
|||
TEARDOWN,
|
||||
}
|
||||
|
||||
enum ControllerOwnership {
|
||||
ITEM_LIST,
|
||||
NOTEPAD_ACTIONS,
|
||||
HOTBAR_MANAGEMENT,
|
||||
HOTBAR_PLACEMENT,
|
||||
}
|
||||
|
||||
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
|
||||
const INVENTORY_MAIN_SIZE := Vector2(882.0, 484.0)
|
||||
const INVENTORY_PANEL_GAP := 16.0
|
||||
|
|
@ -302,6 +317,15 @@ var _last_inventory_section: Section = Section.COOLER
|
|||
var _bag_view: BagView = BagView.EQUIPMENT
|
||||
var _tackle_view: TackleView = TackleView.BAIT
|
||||
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_descending: bool = true
|
||||
var _fish_selection := FishBatchSelectionType.new()
|
||||
|
|
@ -589,12 +613,25 @@ func set_profile_preview_world_pixel_size(pixel_size: int) -> void:
|
|||
_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:
|
||||
if event is InputEventKey and event.echo:
|
||||
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):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_controller_secondary_switch(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_direct_page_shortcut(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
@ -613,47 +650,381 @@ func _input(event: InputEvent) -> void:
|
|||
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:
|
||||
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
|
||||
var use_mapping: bool = (
|
||||
_controller_mapping_manager != null
|
||||
)
|
||||
var uses_left_bumper: bool = (
|
||||
var uses_left_trigger: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event, ControllerMappingManagerType.ROLE_LB
|
||||
event, ControllerMappingManagerType.ROLE_POINTER_MODIFIER
|
||||
)
|
||||
if use_mapping
|
||||
else (
|
||||
button_event != null
|
||||
and button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
|
||||
motion_event != null
|
||||
and motion_event.axis == JOY_AXIS_TRIGGER_LEFT
|
||||
)
|
||||
)
|
||||
var uses_right_bumper: bool = (
|
||||
var uses_right_trigger: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event, ControllerMappingManagerType.ROLE_RB
|
||||
event, ControllerMappingManagerType.ROLE_CAMERA_ZOOM
|
||||
)
|
||||
if use_mapping
|
||||
else (
|
||||
button_event != null
|
||||
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||
motion_event != null
|
||||
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
|
||||
)
|
||||
)
|
||||
if (
|
||||
button_event == null
|
||||
or not (uses_left_bumper or uses_right_bumper)
|
||||
not (uses_left_trigger or uses_right_trigger)
|
||||
or not visible
|
||||
):
|
||||
return false
|
||||
if not button_event.pressed:
|
||||
var is_pressed: bool = (
|
||||
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
|
||||
# Shoulder input belongs to the Player Menu while it is visible, even when
|
||||
# a transition or modal temporarily prevents changing pages. This keeps LB
|
||||
# from opening Chat and RB from leaking into gameplay behind the menu.
|
||||
if (
|
||||
(uses_left_trigger and _left_page_trigger_held)
|
||||
or (uses_right_trigger and _right_page_trigger_held)
|
||||
):
|
||||
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 (
|
||||
_transitioning
|
||||
or _page_transitioning
|
||||
or _sale_confirmation.visible
|
||||
or get_viewport().gui_is_dragging()
|
||||
or _controller_ownership != ControllerOwnership.ITEM_LIST
|
||||
):
|
||||
return true
|
||||
var sections: Array[Section] = [
|
||||
|
|
@ -670,13 +1041,169 @@ func _handle_controller_page_switch(event: InputEvent) -> bool:
|
|||
if current_index < 0:
|
||||
current_index = 0
|
||||
var direction: int = (
|
||||
-1 if uses_left_bumper else 1
|
||||
-1 if uses_left_trigger else 1
|
||||
)
|
||||
var next_index: int = wrapi(current_index + direction, 0, sections.size())
|
||||
_show_section(sections[next_index])
|
||||
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:
|
||||
var key_event := event as InputEventKey
|
||||
if (
|
||||
|
|
@ -772,6 +1299,7 @@ func open_menu() -> void:
|
|||
or not _fishing_spot.can_open_player_menu()
|
||||
):
|
||||
return
|
||||
_release_controller_ownership(false, true)
|
||||
_menu_generation += 1
|
||||
_transition_generation += 1
|
||||
_cancel_presentation_tween()
|
||||
|
|
@ -791,6 +1319,9 @@ func open_menu() -> void:
|
|||
_update_shell_layout()
|
||||
_begin_menu_entry()
|
||||
menu_visibility_changed.emit(true)
|
||||
inventory_hotbar_context_changed.emit(
|
||||
_current_section in [Section.COOLER, Section.BAG]
|
||||
)
|
||||
|
||||
|
||||
func open_section(section: Section) -> bool:
|
||||
|
|
@ -921,6 +1452,7 @@ func close_menu(
|
|||
if reason != CloseReason.USER:
|
||||
_finish_close(reason, restore_controls, _menu_generation)
|
||||
return
|
||||
_release_controller_ownership(false, true)
|
||||
get_viewport().gui_cancel_drag()
|
||||
_close_sale_confirmation()
|
||||
if reason in [
|
||||
|
|
@ -949,6 +1481,7 @@ func close_for_session_end() -> void:
|
|||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_release_controller_ownership(false, true)
|
||||
_cancel_presentation_tween()
|
||||
_cancel_page_tween()
|
||||
if visible:
|
||||
|
|
@ -1007,6 +1540,7 @@ func _show_section(section: Section) -> void:
|
|||
and _profile_page.request_close_confirmation()
|
||||
):
|
||||
return
|
||||
_release_controller_ownership(false, true)
|
||||
_begin_page_transition(section)
|
||||
|
||||
|
||||
|
|
@ -1122,22 +1656,39 @@ func _focus_current_section() -> void:
|
|||
if _shop_cooler_context_active:
|
||||
_focus_shop_cooler()
|
||||
return
|
||||
if _current_section == Section.COOLER:
|
||||
_cooler_sub_tab.grab_focus()
|
||||
elif _current_section == Section.BAG:
|
||||
_bag_sub_tab.grab_focus()
|
||||
elif _current_section == Section.TACKLE_BOX:
|
||||
_tackle_sub_tab.grab_focus()
|
||||
elif _current_section == Section.LOGBOOK:
|
||||
_catalog_logbook.focus_initial()
|
||||
elif _current_section == Section.NET:
|
||||
_the_net_page.focus_initial()
|
||||
elif _current_section == Section.MAIL:
|
||||
_mail_tab.grab_focus()
|
||||
elif _current_section == Section.PLAYERS:
|
||||
_players_tab.grab_focus()
|
||||
else:
|
||||
_profile_tab.grab_focus()
|
||||
_reserve_main_navigation_for_page_switching()
|
||||
_reserve_visible_secondary_navigation()
|
||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||
var candidates: Array[Control] = []
|
||||
_collect_focusable_content(self, navigation_cluster, candidates)
|
||||
if candidates.is_empty():
|
||||
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_content(
|
||||
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:
|
||||
|
|
@ -1161,6 +1712,7 @@ func _process(delta: float) -> void:
|
|||
|
||||
|
||||
func _configure_navigation_focus() -> void:
|
||||
_reserve_main_navigation_for_page_switching()
|
||||
var navigation: Array[BubbleButtonType] = [
|
||||
_inventory_tab,
|
||||
_logbook_tab,
|
||||
|
|
@ -1182,6 +1734,48 @@ func _configure_navigation_focus() -> void:
|
|||
bubble.focus_neighbor_right = bubble.get_path_to(next)
|
||||
bubble.focus_neighbor_top = bubble.focus_neighbor_left
|
||||
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] = [
|
||||
_cooler_sub_tab,
|
||||
_tackle_sub_tab,
|
||||
|
|
@ -1359,6 +1953,7 @@ func _refresh_tackle_box() -> void:
|
|||
row.text = "%s ×%d" % [item.display_name, owned.quantity]
|
||||
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
row.toggle_mode = true
|
||||
row.set_meta(&"controller_tackle_item_id", owned.item_id)
|
||||
row.button_pressed = owned.item_id == _selected_tackle_item_id
|
||||
if item.is_bait() and item.icon != null:
|
||||
_apply_tackle_bait_button_style(row)
|
||||
|
|
|
|||
|
|
@ -336,7 +336,32 @@ func _confirm(text: String, action: Callable) -> void:
|
|||
|
||||
|
||||
func _focus_first() -> void:
|
||||
for child: Node in _tabs.get_children():
|
||||
if child is Button and child.visible and not child.disabled:
|
||||
child.grab_focus()
|
||||
return
|
||||
var candidates: Array[Control] = []
|
||||
_collect_focusable_player_controls(self, candidates)
|
||||
if candidates.is_empty():
|
||||
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,6 +1,10 @@
|
|||
class_name SettingsBubblePage
|
||||
extends Control
|
||||
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
|
||||
@export var page_id: StringName
|
||||
@export var cluster_path: NodePath = ^"BubbleCluster"
|
||||
@export var bubble_paths: Array[NodePath] = []
|
||||
|
|
@ -169,23 +173,13 @@ func _update_layout() -> void:
|
|||
_cluster.position = _resting_cluster_position
|
||||
_cluster.size = field_size
|
||||
_cluster.apply_layout(field_size, compact)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
||||
|
||||
|
||||
func _configure_focus_order() -> void:
|
||||
for bubble: BubbleButton in _bubbles:
|
||||
bubble.focus_mode = Control.FOCUS_NONE
|
||||
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
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_bubbles)
|
||||
|
||||
|
||||
func _set_interactive(interactive: bool) -> void:
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ enum PresentationMode {
|
|||
@onready var _mouse_value: BubbleButton = %MouseValue
|
||||
@onready var _controller_value: BubbleButton = %ControllerValue
|
||||
@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_interval: BubbleButton = %AutoClickIntervalValue
|
||||
|
||||
|
|
@ -84,6 +85,7 @@ var _auto_click_interval_value: float = 0.20
|
|||
var _mouse_sensitivity: float = 0.005
|
||||
var _controller_sensitivity: float = 2.5
|
||||
var _invert_camera_y: bool = false
|
||||
var _on_screen_keyboard_enabled: bool = false
|
||||
var _network_profile: NetworkProfilePreferences
|
||||
var _network_session: NetworkSession
|
||||
var _data_root: PlayerDataRoot
|
||||
|
|
@ -161,6 +163,7 @@ func _ready() -> void:
|
|||
_adjust_controller_sensitivity.bind(1)
|
||||
)
|
||||
_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)
|
||||
for control: Control in [_auto_click_toggle, _auto_click_interval]:
|
||||
control.mouse_entered.connect(
|
||||
|
|
@ -704,6 +707,7 @@ func _apply_settings() -> void:
|
|||
edited.mouse_camera_sensitivity = _mouse_sensitivity
|
||||
edited.controller_camera_sensitivity = _controller_sensitivity
|
||||
edited.invert_camera_y = _invert_camera_y
|
||||
edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled
|
||||
if _settings_manager.apply_settings(edited):
|
||||
if (
|
||||
_presentation_mode == PresentationMode.TITLE_EMBEDDED
|
||||
|
|
@ -725,6 +729,7 @@ func _load_controls() -> void:
|
|||
_mouse_sensitivity = settings.mouse_camera_sensitivity
|
||||
_controller_sensitivity = settings.controller_camera_sensitivity
|
||||
_invert_camera_y = settings.invert_camera_y
|
||||
_on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled
|
||||
_refresh_value_labels()
|
||||
|
||||
|
||||
|
|
@ -755,6 +760,10 @@ func _refresh_value_labels() -> void:
|
|||
"invert\nvertical\ncamera\n"
|
||||
+ ("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 = (
|
||||
"accessibility\nauto-click\n"
|
||||
+ ("on" if _auto_click_enabled else "off")
|
||||
|
|
@ -883,6 +892,11 @@ func _toggle_invert_y() -> void:
|
|||
_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:
|
||||
_auto_click_enabled = not _auto_click_enabled
|
||||
_refresh_value_labels()
|
||||
|
|
|
|||
|
|
@ -397,8 +397,8 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
script = ExtResource("3_page")
|
||||
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/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")])
|
||||
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")])
|
||||
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")])
|
||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||
|
||||
|
|
@ -491,17 +491,29 @@ neutral_size = Vector2(176, 170)
|
|||
compact_minimum_size = Vector2(170, 164)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 24
|
||||
desktop_anchor = Vector2(525, 145)
|
||||
compact_anchor = Vector2(485, 135)
|
||||
desktop_anchor = Vector2(525, 105)
|
||||
compact_anchor = Vector2(485, 95)
|
||||
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")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "map\ncontroller"
|
||||
neutral_size = Vector2(160, 150)
|
||||
desktop_anchor = Vector2(525, 330)
|
||||
compact_anchor = Vector2(485, 300)
|
||||
desktop_anchor = Vector2(525, 390)
|
||||
compact_anchor = Vector2(485, 355)
|
||||
compact_minimum_size = Vector2(148, 138)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 23
|
||||
|
|
@ -512,8 +524,8 @@ unique_name_in_owner = true
|
|||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "back"
|
||||
neutral_size = Vector2(100, 94)
|
||||
desktop_anchor = Vector2(635, 445)
|
||||
compact_anchor = Vector2(555, 405)
|
||||
desktop_anchor = Vector2(635, 470)
|
||||
compact_anchor = Vector2(555, 420)
|
||||
compact_minimum_size = Vector2(84, 80)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 17
|
||||
|
|
|
|||
|
|
@ -608,6 +608,10 @@ func _prepare_awaiting_start_input() -> void:
|
|||
_navigation_focus_active = false
|
||||
_modal_restore_navigation_focus = false
|
||||
_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.modulate.a = 0.0
|
||||
_continue_stats_hovered = false
|
||||
|
|
@ -650,6 +654,10 @@ func _reveal_primary_menu() -> void:
|
|||
):
|
||||
return
|
||||
_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()
|
||||
_title_entry_generation += 1
|
||||
_title_entry_transition_active = true
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(256, 180)
|
|||
const UIReferencePresentationType = preload(
|
||||
"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(
|
||||
requested_pixel_size: int,
|
||||
|
|
@ -16,6 +23,9 @@ signal effective_pixel_size_changed(
|
|||
@onready var _canonical_stage: Control = (
|
||||
$UIViewport/GameUI/UIRoot/CanonicalStage
|
||||
)
|
||||
@onready var _hotbar: HotbarUI = (
|
||||
$UIViewport/GameUI/UIRoot/CanonicalStage/Hotbar
|
||||
)
|
||||
@onready var _chat_ui: ChatUI = $UIViewport/GameUI/UIRoot/ChatUI
|
||||
@onready var _title_content_stage: Control = (
|
||||
$UIViewport/GameUI/UIRoot/TitleScreen/ResponsiveTitleStage
|
||||
|
|
@ -26,14 +36,25 @@ var _effective_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE
|
|||
var _gameplay_active: bool = false
|
||||
var _interactive_ui_open: bool = false
|
||||
var _passive_pointer_ui_enabled: bool = false
|
||||
var _on_screen_keyboard: OnScreenKeyboardType
|
||||
|
||||
|
||||
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()
|
||||
root_viewport.size_changed.connect(_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:
|
||||
_requested_pixel_size = clampi(
|
||||
pixel_size,
|
||||
|
|
@ -120,6 +141,7 @@ func _resize_presentation() -> void:
|
|||
UIReferencePresentationType.get_stage_position(display_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.position = _canonical_stage.position
|
||||
_title_content_stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# World authoring
|
||||
|
||||
The active test world uses the human-authored starter island through
|
||||
The active test world uses the canonical starter island through
|
||||
`world/regions/starter_island_region.tscn`.
|
||||
|
||||
## Active composition
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue