4 Commits

Author SHA1 Message Date
9ea573d681 Add Diablo-style combat: player health/attack/click-move, enemies, tile-based arenas
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.
2026-08-27 22:31:47 +02:00
6eb6758216 Right-size POIs and fix interaction prompt visibility + terrain snap
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).
2026-08-27 10:05:11 +02:00
3d1933399e Fix POI hitbox/sprite mismatch and tight interaction range
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.
2026-08-27 09:47:57 +02:00
c6d7e6c84e Restructure project into boot/autoload/world/player folders
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.
2026-08-27 09:18:17 +02:00