Player (player/player.gd, shared by both island.tscn's real Player and
the player/player.tscn debug prototype via the same script):
- Health (max_health/health, take_damage, heal_full, death via
change_scene_to_file to a new Game Over screen).
- Left-click move-to-point layered onto existing WASD (WASD cancels an
active click-target and takes priority; click-move re-derives the
existing screen-relative animation logic from the world-space
direction instead of duplicating it). New "attack" input action
(Space) does a simple range check against everything in the
"enemies" group. Runs via _unhandled_input so an open interaction
menu naturally takes priority (button consumes the event first).
New systems:
- world/enemy/enemy.gd + .tscn: direct-chase CharacterBody3D (no
pathfinding), contact damage with its own cooldown, reports death to
ArenaManager rather than freeing itself (which owns the live count).
- world/arena/tiles/: 6 placeholder tile scenes (4 plain floor colors,
2 with a simple obstacle pillar) — pure static geometry, same
unshaded-flat-color convention as terrain.gd.
- autoload/arena_manager.gd (new singleton): assembles a 3x3 grid of
random tiles (center forced to plain floor) under a persistent
ArenaRoot positioned far below the island (0,-300,0, group
"arena_root"), rebuilding it fresh on every trigger since only one
arena is ever active. Adds 4 axis-aligned invisible walls (same idea
as terrain.gd's _build_edge_barrier, no rotation needed for a
square). Spawns 3 enemies, teleports the player in. On the last
enemy's death: calls the POI's mark_visited(), reuses
InteractionManager.open_message() (existing, previously-unused code)
for a "Sieg!" message, then teleports the player back after a timer.
- world/poi/poi_arena.gd + .tscn: same Area3D/sprite shell as poi.gd,
but interacting starts an arena directly instead of opening the
generic menu (same divergence pattern poi_gate.gd already
established). Exposes the same duck-typed `visited` field, so
poi_gate.gd's region-gate group check needed zero changes.
- boot/game_over_screen.gd + .tscn: same Control/Panel/Button
structure as main_menu.tscn, one button back to the main menu.
- world/ui/player_hud.gd + .tscn: minimal CanvasLayer HP label.
Content pass: poi.gd gained `event_text` (falls back to the old flat
placeholder if unset); interaction_manager.gd's "explore" case uses it
and "rest" now actually calls player.heal_full(). Applied to region 1:
Ort1/Ort4/Ort6 became arena POIs, Ort2/Ort3/Ort5 got distinct event
text — first concrete proof of the "mix of events and arenas" pattern,
replicating to regions 2-6 is a fast follow-up.
Verified headless via direct method calls (confirmed limitation:
synthetic clicks/keypresses don't reliably drive _unhandled_input in
this harness): start_arena produces 9 tiles + a wall body + 3 enemies
and teleports the player near y=-300; killing all 3 enemies flips the
POI's visited flag and opens the win message; completing the win flow
returns the player next to the original POI; marking the other 5
region-1 POIs visited then correctly unlocks GateR1toR2 (proving the
arena-POI/gate integration without touching poi_gate.gd); player
take_damage(max_health) triggers is_dead and a scene change to
game_over_screen.tscn (confirmed after allowing deferred frames to
process). Also verified an enemy actually closes distance on the
player and lands contact damage over a live physics simulation, and
that the player's attack respects both damage and cooldown.
Not verifiable headlessly, needs in-editor testing: real click-to-move
and Space-bar attack end-to-end, combat feel/balance, arena tile
visuals.
Three compounding issues, found via user testing:
1. Sprite was 2.56x2.56 units (raw 256px checkerboard at default
pixel_size) — 5x wider than the player, and collision shapes sized
to roughly match. Gave AnimatedSprite3D an explicit pixel_size
(0.004) for a ~1 unit sprite, shrunk the solid CylinderShape3D to
radius 0.4/height 1.0 and the Area3D trigger sphere to radius 0.6
accordingly (still >= the solid radius, so touching = interactable
holds).
2. InteractionPrompt had zero vertical offset, sitting dead center of
the (huge) sprite — rendering behind/inside it, so the "[F]
Interact" hint never appeared even though proximity detection and
the keypress itself worked (confirmed by the user: pressing F did
work, the hint just never showed telling them to). Gave both
NameLabel and InteractionPrompt proper offsets above the sprite.
3. Once the collision shapes shrank, a pre-existing ~0.7 unit gap
between each POI's hand-placed Y (computed with the same
radius-only height approximation that's only exact along a
facet's straight edges, not its interior — same class of error as
the earlier Ort6 out-of-bounds bug) and the true terrain surface
became consequential: the small collider mostly floated above the
real ground, giving wildly direction-dependent, sometimes entirely
missing contact/interaction depending on approach angle. Root-fixed
rather than re-deriving 36+ positions' exact heights by hand again:
poi.gd/poi_gate.gd now raycast straight down against the terrain
in _ready() (after one physics_frame, so the terrain's collision
shape has had time to register) and snap themselves onto the real
surface plus a small clearance.
Verified headless: approaching a POI from 4 different directions now
gives consistent ~0.56 unit contact distance and player_nearby=true
in all four (previously: two directions barely touched at ~0.05 units
with interaction never triggering, the other two behaved correctly —
direction-dependent failure, now gone). Re-ran the full 6-gate chain
(still passes) and gate-to-border-prop clearance across 3 randomized
layouts (still 1.3-1.9 units clear, no regression).
Both poi.tscn and poi_gate.tscn had two problems stacking on top of
each other: the solid StaticBody3D collider was a fixed-orientation
BoxShape3D (with a Z-offset originally tuned for the old non-billboard
ruins sprite), and the Area3D's own proximity-detection sphere also
carried that same leftover Z-offset. Neither rotates with the sprite,
which now billboards to face the camera (billboard=2) — so depending
on viewing angle you'd either bump into invisible air or walk clean
through the visible sprite, and the interact prompt appeared at wildly
different distances depending on approach direction.
Fix: replaced the oriented box with a centered CylinderShape3D
(radius 1.0) for the solid collider — rotationally symmetric around Y,
so it matches a billboarded sprite from any camera angle by
construction, no per-frame reorientation needed. Removed the Z-offset
from the Area3D's detection sphere and grew its radius to 1.3 (>= the
solid cylinder's radius), so interaction becomes available at or
before physical contact, not after pushing further in.
Verified headless: drove the player at a POI from 4 different
approach angles (0/90/180/270°) and measured both the distance where
movement gets blocked and the distance where player_nearby flips true.
Blocking distance is now consistent (~1.16-1.17 units) across all four
angles instead of varying with direction, and in every case
interaction was ready at or before the blocking distance.
Extends the proven region-1 pattern (6 placeholder POIs + a gate) to
the remaining 5 outer regions, and rewires the gate chain to match the
confirmed progression plan: visit all 6 outer regions in order, then
reach the center — not "region 1 unlocks the center directly" as it
was built initially.
- Repurposed the old GateToCenter into GateR1toR2 (still gated on
region1_pois, now leads into region 2 instead of the center).
- Added GateR2toR3 .. GateR5toR6, each gated on its own region's POI
group, sitting just off the radial ridge between two regions
(2.5 units perpendicular offset — a hard analytical lower bound on
clearance, since border props' jitter+radius can reach at most
~0.75 units off the ridge centerline).
- Added GateR6toCenter (gated on region6_pois) at the region-6 table
edge, using the same positioning approach as the original center
gate.
- 30 new POI instances (R2_Ort1..R6_Ort6), positions computed by
rotating region 1's verified-safe (angle-offset, radius) template by
60° per region rather than re-deriving hex geometry by hand again —
region 1's own Ort6 mistake was exactly this kind of hand-arithmetic
error, not repeating it this time.
Verified headless: all 6 groups have exactly 6 POIs; every gate stays
locked ("Verschlossen", non-interactive) until its whole group is
visited, then unlocks and its "Ja" teleports the player to exactly
target_position — checked for all 6 gates in sequence. Also measured
actual distance from every gate to the nearest border-prop hitbox
across 3 independent runs (border placement is randomized each scene
load) — consistently 1.3-2.2 units clear, no overlaps.
boot/: opening_screen (attribution -> title/flavor text placeholder,
explicitly marked TODO for a later real cinematic, skippable by
keypress/click) -> main_menu (Neues Spiel/Fortsetzen/Optionen/Credits/
Beenden, ConfirmationDialog for overwrite-existing-save) -> options_menu
(TabContainer with 4 empty placeholder categories: Grafik/Sound/
Steuerung/Spieloptionen) and credits_screen, both just navigable stubs.
project.godot's run/main_scene now points at boot/opening_screen.tscn
instead of directly at the gameplay scene.
autoload/save_manager.gd: new SaveManager singleton. Deliberately
minimal — persists only a timestamp to user://savegame.json — but a
real, working has_save()/save_game()/load_game()/start_new_game()
round trip, enough to back the menu's Start/Continue/overwrite-confirm
logic without inventing game state that doesn't exist yet.
player/equipment/: Equipment (Node) + EquipmentItem (Resource) stub
classes per the user's explicit ask for a "class diagram skeleton" to
build on later — exported fields and empty TODO methods, no logic.
Wired as a child of world/island.tscn's real Player (not player.tscn,
which is only a debug prototype scene, not the actual game's player).
player.gd's new `equipment` onready var uses get_node_or_null() since
that child only exists on the real player, not every scene reusing
this script.
Verified headless: all new boot scenes load cleanly, a full boot smoke
test via run/main_scene succeeds, and — since button-click navigation
can't be simulated headlessly — SaveManager's actual save/load/
overwrite/cleanup cycle was exercised directly and behaves correctly
(has_save flips true/false correctly, loaded data matches what was
saved). Manual in-editor check of the menu navigation itself is still
needed and not yet done.
Flat root layout replaced with a domain-based structure:
- autoload/ the two existing singleton scripts (InteractionManager,
InteractionUI) — grouped by role, matching project.godot's
[autoload] section
- world/ the flying island: terrain.gd, and world/poi/ for the POI
marker + gate scripts/scenes
- player/ everything about the player as a character: player.gd/.tscn,
camera_rig.gd (it only exists to follow the player)
- boot/ created empty for now, will hold the opening/menu screens
main.tscn renamed to world/island.tscn — "main" stops making sense once
the actual run/main_scene becomes a boot screen (next commit). Verified
via grep that main.tscn was referenced nowhere else by path (only by UID
elsewhere, which self-heals).
Every .gd got its .uid sidecar moved alongside it. Two ext_resource
entries had no UID (poi_gate.tscn's own script ref, and island.tscn's
ref to poi_gate.tscn) and needed their path= fixed by hand; everything
else is UID-based and resolved itself after a project reimport.
Also removed two untracked-looking but actually committed editor temp
files (preview_scene.tscn*.tmp, leftovers from the unused hterrain
addon, referenced nowhere).
Verified headless: all 8 affected scenes (island, poi, poi_gate,
player, interaction_ui, and the three debug prototypes that reference
moved files) load with exit code 0. No behavior change, pure move.
Placing the gate right at the table-edge chord meant it landed inside
the overlapping border-prop hitboxes there (measured: they reach out
to radius ~14.9 at that angle, the gate was at 13.2 — actually
overlapping by ~0.45 units). Moved it out to radius 16 along the same
30° bisector, just past where the border wall's hitboxes end.
Verified headless by measuring actual distances: closest border-prop
hitbox edge is now 1.9 units clear of the gate's own trigger radius.
- Moved GateToCenter to sit right on the region-1/center table-edge
border (angle 30° bisector, radius ~13.2, where that edge's chord
actually sits) instead of a few units inside the region, and scaled
the instance down to 0.5x.
- target_position now lands just past the border on the table side
(radius ~11.5 at the same angle) instead of teleporting to the
absolute island center — "you walked through the gate," not "you
warped across the map."
- Gate is now genuinely non-interactive while locked: no prompt shown,
_input() doesn't even check for the key press, and the marker shows
"Verschlossen" instead of its real name (mirrors poi.gd's existing
"???" pattern for undiscovered POIs).
- Once unlocked, interacting opens a "Durchgehen?" Ja/Nein menu
instead of teleporting immediately. Added
InteractionManager.handle_interaction_action() delegation so POIs
can own custom menu actions without hardcoding them into the shared
manager.
Verified headless: locked state hides the prompt and shows
"Verschlossen"; unlocking (all 6 region1_pois visited) restores the
real name and prompt; "Nein" leaves the player position untouched;
"Ja" moves the player to exactly target_position.
poi.tscn/poi_gate.tscn's AnimatedSprite3D had no billboard mode set
(default: disabled, fixed orientation in world space), so POI markers
would appear to rotate/distort as the step-rotating camera moved
around them. Set billboard = 2 (Y-locked), matching the convention
already used for the player's own AnimatedSprite3D.
Verified headless: POI, Ort1, and GateToCenter instances all report
billboard mode 2 at runtime.
We only ever built the inner region-to-region borders, never a wall
at the island's own outer rim (girdle) — nothing stopped the player
walking off into the (collision-less) pavilion below. Added
_build_edge_barrier(): 6 invisible BoxShape3D walls along the girdle
chords.
Also found the actual bug behind "one POI is outside": a facet's true
outer boundary is a straight hexagon chord, not an arc, so its usable
radius varies with angle — girdle_radius only at the two corners,
dropping to girdle_radius*cos(30°) (~87%) at the facet's own bisector.
Ort6 was placed at exactly the bisector angle with radius 44, just
past the true edge (~43.3 there) despite being "inside" the naive
[table_radius, girdle_radius] range. Moved it to angle 38°/radius 36,
comfortable inside the true boundary at that angle.
Verified headless: pushing the player outward with constant velocity
for 3 simulated seconds pins it at radius 42.64 (never crosses the
~43.3 true edge), settled and on_floor=true throughout. Re-verified
the POI/gate group logic still passes after moving Ort6.
poi.tscn now uses the checkerboard debug texture instead of the ruins
sprite, and poi.gd gained a marker_color export (multiplied into the
existing visited/undiscovered dimming) so each POI marker can be
tinted a distinct color. Six instances placed inside region k=0's
wedge (angle ~12-48°, radius ~18-38, clear of the border walls),
tagged with the "region1_pois" group, in red/orange/yellow/green/
blue/purple.
Simplified InteractionManager's "Erkunden" content to a plain
placeholder ("Das wurde besucht.") instead of the old random
animal-track flavor text, and removed the now-dead handle_explore()
submenu it drove. Added InteractionManager.open_message() for POIs
that need an info popup without the explore/rest/close menu.
New poi_gate.gd/.tscn: a portal placed near region k=0's table-edge
border. Checks whether every POI in "region1_pois" has been visited;
if not, shows a "not yet" message, otherwise teleports the player to
target_position (the center) — a one-way shortcut past the border
wall rather than an opening in it, matching the "explore everything,
then unlock the way onward" progression the user described (this
pattern will repeat per outer region later).
Verified headless: all 6 POIs register in the group with correct
names/colors, gate refuses the transition at 0/6 and 5/6 visited and
allows it at 6/6, and the teleport lands the player exactly on
target_position.
_build_region_borders() previously only bordered the 6 seams between
adjacent outer regions (table corner -> girdle corner). Generalized
_build_ridge() into _build_border_segment(p0, p1), which chains
overlap-guaranteed props along any straight 3D segment, and reused it
for the 6 table edges (table corner -> next table corner) as well —
these separate the center region from each outer region.
Verified headless the same way as the ridges: 43/43 pairs on one
table edge genuinely overlap (worst case -0.13 units). 744 border
props total across all 12 edges now.
_build_ridge() now picks each prop's real-world collision radius
(shape radius * scale, same values as elsewhere in main.tscn) and
places it so its hitbox is guaranteed to overlap the previous one's
(border_overlap=0.8 of the combined radii), correcting the step
iteratively since the facet is sloped so distance isn't purely
radial. Lateral jitter shrunk to 0.05 so it can't itself exceed the
smallest possible combined radius (two forest props) and break the
guarantee.
Verified headless by measuring actual 3D distance between every
consecutive pair of props on one ridge against their combined radii:
83/83 pairs genuinely overlap, worst case still -0.13 units of
overlap (no gap anywhere). 504 border props total across all 6
ridges (was 142 with the old fixed-spacing approach).
The random step jitter in _build_region_borders() was still ±4.0, a
constant left over from before the island rescale — at the new
border_spacing of 3.5 that could produce near-zero or even negative
steps as often as it produced 7.5-unit gaps, independent of position
along the ridge (hence gaps showing up near the center too, just by
bad luck). Jitter is now proportional to border_spacing (±25%), and
spacing itself tightened to 1.5 for a denser wall.
Verified headless: 142 border props total, max gap along one ridge
now ~1.9 units (was up to 7.5), consistent from table edge to girdle.
Island geometry (table/girdle/crown/pavilion radii, border spacing)
scaled down ~5x. The player capsule, camera distance and mountain/
forest scale factors were already correctly tuned to the small world
scale established elsewhere in main.tscn; the island itself was the
outlier (girdle radius 260 vs. a ~0.5-tall player), making border
props and the player look like specks. Shrinking the island was the
smaller change versus rescaling every already-tuned system.
Player now spawns partway out on one outer facet (region) instead of
on the center table, so a walk toward the middle is an actual journey
around/through the outer regions rather than starting there.
Verified headless with a physics simulation on the new sloped spawn
point: player settles and stays on_floor=true, no fall-through.
Player gets its own CapsuleShape3D (was a small CylinderShape3D, never
actually shared with anything — the shared oversized capsule flagged
earlier only affects the 4 mountain props, unrelated to the player).
Real cause of the fall-through: the crown collision is a zero-thickness
trimesh and the player spawned with exactly zero clearance sitting
right on the surface, a classic tunneling setup. Fixed by giving the
trimesh backface_collision (collide from both sides) and spawning the
player with a bit of headroom so it settles onto the floor instead of
starting embedded in it. Verified headless: player now falls from
spawn height to rest exactly on the surface and stays on_floor=true
over a 3s simulated run.
Promote debug/HexDiamondIsland.gd to terrain.gd at project root now
that main.tscn depends on it, and swap it in for the old flat
checkerboard Ground (Player/Camera/POI untouched, table sits at the
same y=0 the old ground surface used).
Add _build_region_borders(): places mountain.glb/mountain2v5.glb/
forest.glb (same models, scale factors and hitbox sizes already used
elsewhere in main.tscn) continuously along the 6 ridges between table
corners and girdle corners, i.e. the shared edges between adjacent
outer regions.
Not referenced by any scene and too large to push (native binaries +
doc images, ~19MB). Kept on disk locally; re-fetch it if HTerrain is
ever actually adopted.
New standalone debug prototype exploring the "gem-cut" flying island
shape: a flat hexagonal table (center region) surrounded by 6 sloped
trapezoidal facets down to the girdle (6 outer regions), with a
non-walkable 6-sided pavilion pyramid below for the underside. Crown
gets trimesh collision for later player testing; all 6 outer facets
stay visually neutral (alternating flat color only) since theming
comes later. Doesn't touch main.tscn or the old cross-layout prototype.
Godot 4.6 exploration game with player movement, step-rotation camera
rig, and POI interaction system (explore/rest/close menu). main.tscn
is a test scene with placeholder props; debug/ holds unwired terrain
prototypes (voxel river, procedural island mesh, cross-shaped hub