Skip to content

[#766] Adventure mode: possess one agent + WASD + right-click verbs#776

Merged
milkyskies merged 11 commits into
mainfrom
feature/#767.player-controlled-marker
May 14, 2026
Merged

[#766] Adventure mode: possess one agent + WASD + right-click verbs#776
milkyskies merged 11 commits into
mainfrom
feature/#767.player-controlled-marker

Conversation

@milkyskies

Copy link
Copy Markdown
Owner

closes #767
closes #768
closes #771
closes #769

Bundle landing the four foundation sub-issues of the adventure-mode epic (#766) in one PR. The user explicitly asked for one PR rather than four. The epic itself stays open — HUD auto-pin, ESC "release control", and greyed-out tooltips are deferred follow-ups noted in the relevant commits.

What lands

  1. PlayerControlled marker (PlayerControlled marker + brain arbitration bypass #767)arbitrate_every_tick and update_rational_planning filter Without<PlayerControlled>. The AI stops thinking for marked entities; biology, perception, action execution, conversations, etc. all keep running normally.
  2. WASD + camera follow (WASD movement + camera-follow for player #768) — held WASD/arrows build a Walk template aimed at the adjacent tile and write it into BrainState.chosen_actions. The existing execution pipeline takes it from there. The "movement-state cooldown" falls out naturally because start_actions skips Walk while one is already active. Camera lerps toward the player at alpha=0.15, suspended during middle-click drag.
  3. Adventure mode menu entry (Adventure mode menu entry + possession flow #771) — third button on the main menu jumps straight to the create-world form with SimMode::Adventure. Adventure reuses the Debug spawn population and then possesses the first Person.
  4. Right-click context menu (Click-to-act context menu on visible entities #769) — right-click an entity (or empty space within ~24px of yourself) to bring up a verb popup. Whitelist covers Eat/Sleep/Rest/Wait for self, and per-concept verbs for Person, AppleTree/BerryBush/StoneNode/WoodLog, Wolf/Deer, StorageChest, Campfire, LeanTo/House.

No new actions, no new rendering, no new world content — adventure mode is an input-shim layer on top of the existing simulation pipeline.

Test plan

  • Lib test suite (871 passed, 6 ignored, 0 failed)
  • Integration test tests/test_player_controlled_marker.rs (4 passed): AI never plans/arbitrates, biology+perception still tick, held-D walks east, release restores AI control
  • Unit tests in src/agent/player.rs::tests (6): direction reading, diagonal normalization, opposite-key cancel, arrow alias, lerp boundaries
  • Unit tests in src/world/spawner.rs::tests (2): possess marker on existing Person, no-op when empty
  • Unit tests in src/menu.rs::tests for SimMode::Adventure config
  • Unit tests in src/ui/adventure_menu.rs::tests (5): self-menu inventory gating, per-concept verb tables, fallback to empty
  • cargo build --profile ci succeeds
  • Manual: launch app, click Adventure Mode, verify camera follows the spawned human as WASD walks them around
  • Manual: right-click an apple tree → Harvest appears; right-click another person → Wave/Talk/Attack; right-click yourself with food → Eat appears

🤖 Generated with Claude Code

milkyskies and others added 4 commits May 10, 2026 14:01
Foundation for adventure mode (#766). Marks one agent as player-driven
so AI brain decisions stop firing for it while every other system
(biology, perception, action execution, conversations, ...) keeps
running normally. Subsequent issues land input handling, the action
context menu, camera follow, and the menu entry on top of this.

The marker filters two systems:
  * arbitrate_every_tick — Without<PlayerControlled> on its main query
  * update_rational_planning — same

Wakeup emitters and the per-tick alertness/state-hash bookkeeping
all run unconditionally. Player input (next sub-issue) writes
directly into BrainState.chosen_actions, reusing the existing
execution pipeline.

Three tests covering: (1) AI never touches a possessed agent,
(2) biology/perception keep ticking for a possessed agent,
(3) removing the marker restores AI control.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adventure-mode movement on top of the #767 marker. Held WASD/arrow keys
build a Walk template aimed at the adjacent tile and write it into
BrainState.chosen_actions; the existing execution pipeline (start_actions
→ tick_actions → movement) does the rest. The "movement-state cooldown"
falls out naturally — start_actions skips Walk while one is already
active, so held keys re-fire as soon as the previous step lands and the
existing speed model (calculate_speed × intensity × terrain) keeps
authority over per-step pacing.

Camera follow lerps toward the possessed agent each frame at alpha=0.15.
Suspended while middle-click drag is held so manual pan still works;
release lets the lerp pull the camera back. Lerp factored out into a
pure helper for unit tests.

`player_input` takes `Option<Res<ButtonInput<KeyCode>>>` because
TestWorld's MinimalPlugins and headless runs don't include InputPlugin
— the system silently no-ops there instead of panicking on missing
resource validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Main menu gets a third button. Clicking it lands directly on the
create-world form with `SimMode::Adventure` selected, skipping the
mode picker — there's only one Adventure variant so the picker would
be a noise step.

Adventure reuses the Debug spawn population entirely (a real, populated
world is what makes possessing one body interesting); after spawn,
`possess_first_person_for_adventure` marks the first Person as
PlayerControlled. AI keeps running for everyone else.

Empty population is logged as a warning rather than silently leaving
Adventure without a body — a future "no humans in this preset" miswire
should surface loudly.

ESC pause menu and "Release control" are out of scope for the MVP —
quitting to main menu already despawns the world via DespawnOnExit,
which is sufficient until we want mid-session swap-bodies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-click a sprite (or empty space near yourself) to bring up an
egui popup of verbs the player can invoke. Selecting one writes the
matching ActionTemplate into BrainState.chosen_actions, where the
existing execution pipeline picks it up — no new action machinery,
just a new way to drive the same one.

Verb whitelist per concept covers the playable surface for MVP:
  * Self: Eat (only when inventory has food), Sleep, Rest, Wait
  * Person: Wave, Talk, Attack
  * AppleTree/BerryBush/StoneNode/WoodLog: Harvest
  * Wolf/Deer: Attack
  * StorageChest: Take, Deposit
  * Campfire: WarmUp
  * LeanTo/House: RestInShelter

Empty-space right-click within ~24px of the player opens the self
menu — gives the player a forgiving target without having to land
the cursor exactly on their sprite.

Out of scope for the MVP: greyed-out infeasible entries with tooltips
explaining the failed precondition. The current path hides infeasible
verbs entirely; surfacing them as teaching tools is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@milkyskies milkyskies marked this pull request as ready for review May 10, 2026 05:23
milkyskies and others added 7 commits May 10, 2026 14:29
Original held-key model spawned one Walk action per tile-step: walk to
center, action ends with MoveResult::Arrived, next held-key tick admits
a fresh Walk. The gap between actions is the visible step / pause / step
rhythm — not smooth.

Fix: keep a single Walk running for the entire held-key burst. Each
player_input tick mutates the active Walk's target_position to a point
~1.5 tiles ahead of the agent in the held direction. move_toward never
sees Arrived while a key is held, so movement stays continuous at the
existing speed model. Releasing the key freezes the target where it
landed; the agent finishes the trailing step and stops naturally.

Wall clamp: if the lookahead point isn't walkable, the target falls
back to a 0.4-tile-ahead nudge so a player held against rock doesn't
push movement into an unwalkable tile (also caught by Gate::TileReachable
on first admission, but the runtime nudge keeps held-key feel stable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…f-click

Three things the user hit on the live build:

1. Right-clicking yourself opened a concept-less "Target" menu. Cause:
   the pick loop hit one of the silhouette child sprites (head/torso/
   eyes) that share the player's transform but have no EntityType.
   Filter the loop to require EntityType so only real world objects
   match — silhouette parts no longer steal the click and the SELF_-
   PICK_RADIUS fallback fires correctly.

2. Even with berries in inventory, the self-menu showed no Eat verb
   because the original `has_edible(ontology)` check could fail in
   ways that aren't easy to reproduce from a test. Replace the single
   "Eat" entry with one "Eat <Concept> (qty)" entry per food in the
   inventory — checking BOTH `has_trait(_, Edible)` AND `is_a(_,
   Food)` for the membership signal. Belt and suspenders. Player now
   sees exactly what they have to eat.

3. Fish and Drink are now offered in the self-menu when adjacent to
   water (reuses the AdjacentToWater gate's predicate). They were
   missing entirely from the verb whitelist.

4. Adventure mode now auto-pins the character sheet to the possessed
   agent at spawn time — the player sees their own hunger/energy/pain
   without having to left-click themselves first. Implemented by
   selecting the player into `UiState.selected_entities` from the
   same system that inserts PlayerControlled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues from live playtesting:

1. Tap WASD = move ~1.5 tiles. Cause: my smoother-walk lookahead set
   the active Walk's target ~1.5 tiles ahead so it never "arrived"
   while held — but on release, the agent kept walking to that stale
   target. Combined with the 2.5 hop/sec animation, each tap
   coincidentally produced one full hop, hence "one press = one
   unit". Fix: on release, pin the active Walk's target_position to
   current_pos so it arrives within a tick. Also drop the lookahead
   from 1.5 tiles to 0.6 tiles — the only requirement is "agent
   doesn't reach it while keys held," and 0.6 is enough for that
   while making the brief release-overshoot imperceptible.

2. No build verbs. Add BuildLeanTo / BuildHouse / BuildStorageChest
   to the self-menu, gated on inventory wood/stone amounts so the
   menu only offers what the player can actually start.

3. Adventure mode now starts at orthographic scale 0.5 instead of
   1.0 — single-character POV cares about immediate surroundings,
   not a 50×50 overview. The existing scroll-wheel/pinch zoom still
   works, just from a closer-in baseline.

(The "river displacement" the user noticed is the existing terrain
elevation lift in sprite_animation.rs, not a bug — sprites get
raised/lowered to sit on the visual relief, and water tiles drop the
sprite. Logical position is unchanged. Leaving as-is for now.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-tile elevation lookup made sprite Y "teleport" at tile boundaries:
crossing the seam between two tiles of different elevation produced an
instant jump in sprite height. Replace the lookup with a bilinear
sample of the four tile-center elevations surrounding the agent's
sub-tile position. Elevation now varies continuously across the map,
so an agent walking from grass into a riverbank glides down instead
of snapping.

Tile rendering itself still discretizes to per-tile heights — this
only affects the entity sprite layer, which is what reads
`elevation_lift_at`.

Map-edge holes (any of the four corner tiles missing) fall back to
the average of the present corners rather than 0, so sprites don't
plummet at the world boundary.

Three new tests cover the bilinear sampler:
- exact value at a tile center
- midway interpolation between adjacent centers
- no discontinuity across the boundary (the bug this commit fixes)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regression from the smooth-walk fix: when no directional key was held,
player_input cleared `BrainState.chosen_actions` outright every tick.
That clobbered any template the right-click menu had just written —
Harvest, Eat, Fish, Build, etc. all got wiped one tick later before
`start_actions` could admit them. The user noticed: "i cant harvest
any shit anymore from anything."

Fix: filter the clear to Walk only. Other action types persist long
enough to be admitted on the next FixedUpdate. Walk's stop-on-release
behavior is preserved because the active Walk's target_position still
gets pinned to current_pos.

Also stop blanking the agent's `TargetPosition` component on release
— Harvest and friends route through it for proximity navigation, and
unconditional clearing was a second source of dropped actions.

Regression test: drop a Harvest template into BrainState, hold no
keys, tick 5 times, assert Harvest is still there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o-walk + Construct + Campfire

Pile of UX fixes from live playtesting:

* Hold Shift while moving = sprint. Bumps locomotion intensity from 0.5
  (1.2× base speed) to 1.0 (2.0× base speed) and burns anaerobic
  stamina the same way Flee does, so sprinting tires the player like
  it would tire any other agent. Toggle works mid-step without
  re-admitting Walk.

* Menu rows are now full-width clickable (egui::Button with min_size
  matching the popup width) instead of a tiny button inside a wide
  popup whose padding wasn't a click target.

* Greyed-out verbs with hover tooltips. Builds always show — when
  materials are missing, the tooltip names the resource and shows
  have/need (e.g. "Wood: 2/5"). Fish/Drink show greyed when not
  adjacent to water with "Need to be next to water". Teaches the
  player what's possible without making them experiment to discover
  the requirements.

* Click outside the popup dismisses it. Same pattern every other
  context menu uses; matches user expectation.

* Build Campfire is in the menu now (forgotten earlier — needs 3 wood,
  uses ActionType::Build).

* ConstructionSite entities offer "Construct" — without this verb,
  placing a Lean-to / House / Storage Chest just sat there since
  finishing requires labor on the site. The user noticed: "it just
  goes into my doing now but it never gets built."

* Entity-targeted actions (Harvest, Attack, Talk, Take, Deposit, …)
  auto-prepend a Walk step when the player isn't on the target tile.
  start_actions admits Walk first; the targeted action sits in
  chosen_actions waiting for proximity, then admits when the agent
  arrives. The user noticed: "i can harvest, attack blah blah from
  ANY range" — because the original menu wrote raw templates whose
  proximity preconditions weren't auto-injected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gacy stamina helper

1. Idle now actually restores stamina. Before: Idle had intensity 0.0,
   which the effort model multiplies the primitive's recovery profile by,
   zeroing it out. So a "standing still" agent got no stamina regen at
   all. Bumped Idle intensity to 0.4 (matches Rest). The functional
   difference between Idle and Rest stays in completion: Rest auto-ends
   when stamina tops up, Idle continues until something else takes
   priority.

2. Construction sites are visible now. They were spawned headless (no
   Sprite, no Visibility components) by the Build / BuildLeanTo /
   BuildHouse / BuildStorageChest action handlers — the entity existed
   but had nothing to render. Added Visibility plumbing on the root
   spawn and a `sync_construction_site_visuals` system that attaches a
   sandy outlined-square placeholder to any site without one. The
   visual despawns with its parent when becomes_system transforms the
   site into the finished entity.

3. Removed `Stamina::recover()` and the four tests that pinned it. Only
   tests called the function — the production stamina recovery path
   goes through the effort model in `effort.rs`. Per the testing rule
   ("test logic, not plumbing"), the deleted tests were pinning a
   parallel implementation that was about to drift from production
   anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@milkyskies milkyskies merged commit a556b17 into main May 14, 2026
2 checks passed
@milkyskies milkyskies deleted the feature/#767.player-controlled-marker branch May 14, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant