diff --git a/README.md b/README.md index 23bce31c..19e9748a 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Founding builds made before anyone lives in the city, and runs with residents of - Bus stops appear on straight roads next to homes and shops. Buses stop there and also run out of train stations. - Delivery trucks run from industry, and garbage trucks from landfills. - Every disaster sends a crew: fire trucks, ambulances and police drive the road network from the nearest station to the event with sirens flashing, stay until it ends, then leave. -- Boats sail the river. Airports run a plane that takes off, circles the city and lands again; hospitals get a circling helicopter when the model is present. +- Boats sail the river's open water: they float at the surface and turn back at a bridge instead of sailing through it. Airports run a plane that takes off, circles the city and lands again; hospitals get a circling helicopter when the model is present. - Wind farm rotors spin and rooftop beacons blink. ## Rendering @@ -83,7 +83,7 @@ Founding builds made before anyone lives in the city, and runs with residents of `godot --path .` starts in **first person** at street level. Set `GRIDFALL_VIEW=orbit` to play the orbit city builder with the full HUD described below. -- **First person:** WASD or the arrow keys walk, Shift runs, mouse to look (click to capture the pointer), `]` and `[` change time speed (1x, 2x, 4x, 8x), `T` skips to the next morning, `Esc` pauses, `F1` hides the HUD, `F5` saves, `F9` loads. `F` by a car gets in: your character walks round to the driver's door, opens it, climbs in and shuts it (`F` on the way turns back). W/S throttle and brake or reverse, A/D steer, Space handbrake, V switches chase and driver view, and `F` again, once the car slows down, brakes to a stop and climbs out the same way. The car is solid: it bounces off walls, scrapes along them at a glance, shoves the cars it hits (they slide off, then pull back into their lane) and sends bins, benches, planters and other light street props flying. People it runs into stagger back, or go down when it is going fast. On foot, left click shoves whoever stands in front of you. Hold right click to put your guard up (fists up, the camera in closer), and left click then throws a jab and a cross in turn. A light blow makes people reel and step back. A hard one, the third punch in a row or a fast car knocks them down: they lie a while, get up, then shout at you, run off or walk back, and the people nearby who saw it run off or shout too. A resident who was hit remembers it and their mood drops (a shove least, a car most), and the ones who saw it remember it too. Floor someone or keep punching and the police drive out from the nearest station; someone a car knocks down gets an ambulance. The one who was hit and a witness say so in the chat, a crew called to the scene posts a City Alert card, and you hear each blow, swing and fall. `1`-`5` wave, nod, shake your head, shout and dance, and the people around answer. `B` opens the build phone: `1`-`6` pick road, homes, shops, industry, a building (mouse wheel picks which) or bulldoze, left click places on the tile ahead (roads and zones take a start and an end click), right click undoes the start. Each order is paid by the district that owns the land, as if its mayor had ordered it, and one of your two building crews builds it in real time (see Staged construction). The phone says how many crews are free and how long the order takes, a card under the top bar counts down your orders, and `E` on a worker gets a word about the job. The AI activity feed (top right) shows each mayor turn, council ruling, Chaos Director pick, grid order and Gazette front page as it lands. A card shows who decided, the model they run on, whether the answer came live, from a replay, from a fallback or from the offline planner, what they did and why. On foot the mayors take their turns spread over the day. A decision with a place raises a light beacon there. `J` cycles the feed between full, brief and hidden, and `G` routes the GPS to the newest decision with a place. Each of those decisions is also a lead for the Gazette, and you are its stringer: `K` takes the newest on assignment, and a card shows what happened, where and how far, and what the story still needs, while the GPS leads you to the scene. There, a reply from someone you talk to (or the answer when you ask someone here) is your quote. In photo mode (`P`), a click with the scene inside the viewfinder's marks is a press photo. `K` then files the story: the Gazette runs it in the chat, the person quoted remembers it, and it earns points towards the press ranks, with bonuses for a scoop (filed within a third of a day), a local voice and a sharp photo. A lead goes cold after a day. `L` opens the mayors' league: the five mayors ranked on people, jobs, approval and funds, each with the model they run on, your press card, the open leads (chase any of them) and your latest stories. +- **First person:** WASD or the arrow keys walk, Shift runs, mouse to look (click to capture the pointer), `]` and `[` change time speed (1x, 2x, 4x, 8x), `T` skips to the next morning, `Esc` pauses, `F1` hides the HUD, `F5` saves, `F9` loads. `F` by a car gets in: your character walks round to the driver's door, opens it, climbs in and shuts it (`F` on the way turns back). W/S throttle and brake or reverse, A/D steer, Space handbrake, V switches chase and driver view, and `F` again, once the car slows down, brakes to a stop and climbs out the same way. The car is solid: it bounces off walls, scrapes along them at a glance, shoves the cars it hits (they slide off, then pull back into their lane) and sends bins, benches, planters and other light street props flying. People it runs into stagger back, or go down when it is going fast. On foot, left click throws a jab and a cross in turn at whoever stands in front of you (your character turns to face them), and `R` shoves them. Hold right click to put your guard up: fists up and the camera in closer. A light blow makes people reel and step back. A hard one, the third punch in a row or a fast car knocks them down: they lie a while, get up, then shout at you, run off or walk back, and the people nearby who saw it run off or shout too. A resident who was hit remembers it and their mood drops (a shove least, a car most), and the ones who saw it remember it too. Floor someone or keep punching and the police drive out from the nearest station; someone a car knocks down gets an ambulance. The one who was hit and a witness say so in the chat, a crew called to the scene posts a City Alert card, and you hear each blow, swing and fall. `1`-`5` wave, nod, shake your head, shout and dance, and the people around answer. `B` opens the build phone: `1`-`6` pick road, homes, shops, industry, a building (mouse wheel picks which) or bulldoze, left click places on the tile ahead (roads and zones take a start and an end click), right click undoes the start. Each order is paid by the district that owns the land, as if its mayor had ordered it, and one of your two building crews builds it in real time (see Staged construction). The phone says how many crews are free and how long the order takes, a card under the top bar counts down your orders, and `E` on a worker gets a word about the job. The AI activity feed (top right) shows each mayor turn, council ruling, Chaos Director pick, grid order and Gazette front page as it lands. A card shows who decided, the model they run on, whether the answer came live, from a replay, from a fallback or from the offline planner, what they did and why. On foot the mayors take their turns spread over the day. A decision with a place raises a light beacon there. `J` cycles the feed between full, brief and hidden, and `G` routes the GPS to the newest decision with a place. Each of those decisions is also a lead for the Gazette, and you are its stringer: `K` takes the newest on assignment, and a card shows what happened, where and how far, and what the story still needs, while the GPS leads you to the scene. There, a reply from someone you talk to (or the answer when you ask someone here) is your quote. In photo mode (`P`), a click with the scene inside the viewfinder's marks is a press photo. `K` then files the story: the Gazette runs it in the chat, the person quoted remembers it, and it earns points towards the press ranks, with bonuses for a scoop (filed within a third of a day), a local voice and a sharp photo. A lead goes cold after a day. `L` opens the mayors' league: the five mayors ranked on people, jobs, approval and funds, each with the model they run on, your press card, the open leads (chase any of them) and your latest stories. The orbit view (`GRIDFALL_VIEW=orbit`): diff --git a/data/feature_flags.json b/data/feature_flags.json index e4d41e8e..f84e3f5a 100644 --- a/data/feature_flags.json +++ b/data/feature_flags.json @@ -17,7 +17,7 @@ "activity": {"default": true, "description": "First-person feature: AI activity feed, decision beacons and map pings showing which mayor or agent decided what, on which model, from which source, and where; mayors take staggered turns through the day on foot. J cycles the feed, G routes to the newest decision."}, "crews": {"default": true, "description": "First-person feature: the player's two building crews. Phone orders are built in real time (2-5 minutes for a building) by crews you can watch on site, with a board and a HUD card counting down; later orders queue for a free crew."}, "props": {"default": true, "description": "First-person feature: street props the player's car knocks over. Bins, benches, planters, mailboxes, bike racks and parking meters hit hard enough tumble off, come to rest and can be hit again; they stand up again out of sight. Slower hits and lamps, trees, bollards and hydrants hold like walls."}, - "actions": {"default": true, "description": "First-person feature: the hero's body actions. Left mouse (right trigger) shoves whoever stands in front; with the guard up (right mouse or left trigger held) it throws a jab and a cross in turn. People reel or go down, the street reacts and the city hears of it. Keys 1-5 wave, nod, shake the head, shout and dance, and the people around answer."}, + "actions": {"default": true, "description": "First-person feature: the hero's body actions. Left mouse (right trigger) throws a jab and a cross in turn and R shoves; the guard (right mouse or left trigger held) is the fighting stance. People reel or go down, the street reacts and the city hears of it. Keys 1-5 wave, nod, shake the head, shout and dance, and the people around answer."}, "reporter": {"default": true, "description": "First-person feature: the player as the Gazette's stringer. Every AI decision with a place is a lead: K takes one on assignment and sets a waypoint to the scene, a quote from someone there (a talk reply or the Ask someone here answer) and an optional press photo (photo mode, scene in the marks) make the story, and K files it into the chat for points and press ranks. L opens the mayors' league: the five mayors ranked, the press card, the open leads and the latest stories."}, "gmi_live": {"default": true, "description": "Kill switch for live GMI Cloud calls. Off: every call uses the replay cache or heuristics, as with GRIDFALL_OFFLINE=1."}, "analytics": {"default": false, "description": "Local product analytics to user://ops/analytics.jsonl (also GRIDFALL_ANALYTICS=1). Nothing is sent anywhere."}, diff --git a/docs/api/scripts.fp.actions.md b/docs/api/scripts.fp.actions.md index 71382e51..53b02f37 100644 --- a/docs/api/scripts.fp.actions.md +++ b/docs/api/scripts.fp.actions.md @@ -8,14 +8,14 @@ extends `FpFeature` -Body actions (integration slice; docs/fp/ARCHITECTURE.md, Body actions): the hero's fists and gestures on foot. The strike button (left mouse, right trigger) shoves whoever stands in front; with the guard up (right mouse or left trigger held: fists up, the body squared to where the camera looks, the camera in closer) it throws a jab and a cross in turn instead. A blow lands when the rig's fist is furthest out (HeroRig.strike_time) on the person nearest in front within REACH, through the traffic feature's hit_walker: they reel or go down, the people around react and the city hears of it. Keys 1-5 wave, nod, shake the head, shout and dance, and the people around answer (traffic answer_emote). The bus hears every swing and emote (hero_action). +Body actions (integration slice; docs/fp/ARCHITECTURE.md, Body actions): the hero's fists and gestures on foot. The strike button (left mouse, right trigger) throws a jab and a cross in turn, the body turning to whoever it goes for (else to where the camera looks); R shoves. The guard (right mouse or left trigger held: fists up, the body squared to where the camera looks, the camera in closer) is the fighting stance. A blow lands when the rig's fist is furthest out (HeroRig.strike_time) on the person nearest in front within REACH, through the traffic feature's hit_walker: they reel or go down, the people around react and the city hears of it. Keys 1-5 wave, nod, shake the head, shout and dance, and the people around answer (traffic answer_emote). The bus hears every swing and emote (hero_action). ### Constants - `WS = preload("res://scripts/fp/world_scale.gd")` - `SWINGS = { "jab": ["punch_jab", "punch", 0.45, 0.4, 0.55, 0.2], "cross": ["punch_cross", "punch"...`: Swings: [rig clip, hit_walker cause, strength 0..1, seconds until the next swing may start, seconds the clip plays, contact time (s) when the rig cannot measure it]. - `EMOTES = { "emote_wave": ["wave", 0.0], "emote_yes": ["yes", 0.0], "emote_no": ["no", 2.0], "emo...`: Emote input actions: [emote, seconds it plays (0 for the clip's own length)]. -- `INPUTS = [ "act_strike", "act_aim", "emote_wave", "emote_yes", "emote_no", "emote_shout", "emote...` +- `INPUTS = [ "act_strike", "act_shove", "act_aim", "emote_wave", "emote_yes", "emote_no", "emote_s...` - `REACH = 1.2 * WS.M`: Who a swing can reach: on their feet within REACH (feet to feet) inside the cone ahead (the cosine); at contact still within REACH * HOLD_REACH. - `CONE = 0.4` - `HOLD_REACH = 1.3` @@ -46,7 +46,7 @@ Body actions (integration slice; docs/fp/ARCHITECTURE.md, Body actions): the her - `func teardown() -> void` - `func self_test() -> Dictionary` - `func can_act() -> bool`: Whether the hero may swing, guard or emote now: on foot and in control of the body, nothing modal open, not talking, not in photo mode, the phone put away. -- `func strike() -> bool`: Starts a swing: a shove, or with the guard up the next punch of the combo. A press near the end of a swing queues the next. False when none starts now. +- `func strike(what: String = "punch") ->`: Starts a swing: what "punch" throws the next punch of the combo (a jab, then a cross within COMBO_S of the last), "shove" a shove. A press near the end of a swing queues the next. False when none starts now. - `func set_guard(on: bool) -> void`: Raises or drops the guard for scripted play (tests, the photo tour); held input does the same. - `func emote(e: String) -> bool`: Plays an emote ("wave", "yes", "no", "shout", "dance") and lets the people around answer it. False when the hero cannot act now, swings or guards, the last emote was just now, or the dance is asked for on the move. - `func cancel() -> void`: Ends the swing, the guard and any action on the rig, and gives the body and camera back. diff --git a/docs/api/scripts.fp.drive.md b/docs/api/scripts.fp.drive.md index 3405d831..d1e3509f 100644 --- a/docs/api/scripts.fp.drive.md +++ b/docs/api/scripts.fp.drive.md @@ -174,7 +174,7 @@ Getting into cars and driving them. Near a parked or stopped car, F (Y on a pad) - `func exit(force: bool = false) -> bool`: Gets out at once beside the driver's door (force: at any speed, wherever there is room or on the spot); also ends a climb in or out half way (a hero still outside the car stays where it stands). Returns true when the hero is on foot again. - `func climb_step(dt: float) -> void`: One step of the climb in or out, or of braking to a stop for the climb out; _process runs it, tests call it with their own dt. - `func drive_step(throttle: float, brake: float, steer: float, hand: float, dt: float) -> void`: One driving step with the given controls (throttle, brake, handbrake 0..1, steer -1..1). -- `func camera_xform() -> Transform3D`: The camera now: behind and above the car (pulled in front of walls), or at the driver's eyes. +- `func camera_xform() -> Transform3D`: The camera now: behind and above the car (pulled in front of walls, lifted off the ground when it looks up from low down), or at the driver's eyes. - `func photo_shots() -> Array`: The photo shot: the hero half way into the car nearest the spawn point, sat on the sill with the door wide open, seen from the door camera (car_doors). - `func photo_prepare() -> void`: Photo tour: the tour puts the hero at the spawn point after this, so the climb is staged once traffic has filled the street round it. - `func photo_restore() -> void` diff --git a/docs/api/scripts.world.md b/docs/api/scripts.world.md index b96afb2d..69471213 100644 --- a/docs/api/scripts.world.md +++ b/docs/api/scripts.world.md @@ -39,6 +39,7 @@ Moving city life layered on top of CityView: buses that stop at bus stops, deliv - `M = "res://models/"` - `ROAD_SCALE = 0.17` - `DIRS = [Vector2i(0, -1), Vector2i(1, 0), Vector2i(0, 1), Vector2i(-1, 0)]` +- `BOB = 0.006`: How far boats bob above and below the water surface (u). - `RESPONSE = { "fire": [["firetruck", D.FIRE], ["firetruck", D.FIRE], ["ambulance", D.HOSPITAL]], "t...`: Which crews answer each disaster type; the first entry is the station kind they leave from. - `SIREN_COLORS = { "firetruck": [Color(1.0, 0.15, 0.1), Color(1.0, 0.75, 0.2)], "ambulance": [Color(1.0,...` diff --git a/docs/fp/ARCHITECTURE.md b/docs/fp/ARCHITECTURE.md index 739d0230..23b74bb9 100644 --- a/docs/fp/ARCHITECTURE.md +++ b/docs/fp/ARCHITECTURE.md @@ -97,7 +97,7 @@ Camera ownership: A owns every camera behaviour (follow, collision, V toggle, th - `city_sim.gd`: `var day_seconds: float = SimDefs.BASE_DAY_SECONDS`, read wherever `BASE_DAY_SECONDS` is used today. - `city_brain.gd` and `sim_defs.gd`: `MAYORS[i]["display"]` (human name). The display name replaces the model name in prompts, `city_log`, `owner_name()` and inspector text. `model` stays for the API. The offline Gazette editorial and the inspector prompt stop saying "AI mayors". - S0's own FP files also carry shared contracts that slices build on: `GameBus.card_action`, `CityDirectory.planned_name`, `CollisionWorld.boxes()`, `AP.load_scene()` and the GLB node-name prefixes, `FpPlayer.photo_mode`, and the `fp_kit` test context. S0 §23 (D-S0-17 to D-S0-22) lists them. -- `project.godot`: FP input actions (`move_forward`, `move_back`, `move_left`, `move_right`, `sprint`, `crouch`, `jump`, `interact`, `chat_open`, `map_toggle`, `pause_menu`, `time_faster`, `time_slower`, `time_skip`, `photo_mode`, `hud_toggle`, `quick_save`, `quick_load`) and, for D13, the camera actions `view_toggle` (V), `shoulder_swap` (Q), `zoom_in` / `zoom_out` (mouse wheel up / down) and `look_left` / `look_right` / `look_up` / `look_down` (gamepad right stick, dead zone 0.15). The existing `camera_*` actions stay. The integration overview adds `overview_toggle` (Tab, gamepad Back), `site_next` (N), `site_prev` (B), `site_go` (F) and `site_waypoint` (G). Driving adds `vehicle_enter` (F, gamepad Y; on foot it only takes the press with a car in reach, so Y still opens the map elsewhere), `drive_throttle` (right trigger) and `drive_brake` (left trigger); on the keyboard the move keys drive. The phone adds `phone_toggle` (B); while it is open it reads 1-6, the mouse buttons and the wheel itself. The AI activity feed adds `ai_feed` (J, keyboard only: every pad button is taken on foot) and reuses `site_waypoint` (G) on foot to route to the newest decision; the pad's Y keeps opening the map. Body actions add `act_strike` (left mouse, right trigger), `act_aim` (right mouse, left trigger; on foot only, as the drive feature reads the triggers at the wheel) and `emote_wave`, `emote_yes`, `emote_no`, `emote_shout`, `emote_dance` (1-5, keyboard only; the open phone reads 1-6 itself). +- `project.godot`: FP input actions (`move_forward`, `move_back`, `move_left`, `move_right`, `sprint`, `crouch`, `jump`, `interact`, `chat_open`, `map_toggle`, `pause_menu`, `time_faster`, `time_slower`, `time_skip`, `photo_mode`, `hud_toggle`, `quick_save`, `quick_load`) and, for D13, the camera actions `view_toggle` (V), `shoulder_swap` (Q), `zoom_in` / `zoom_out` (mouse wheel up / down) and `look_left` / `look_right` / `look_up` / `look_down` (gamepad right stick, dead zone 0.15). The existing `camera_*` actions stay. The integration overview adds `overview_toggle` (Tab, gamepad Back), `site_next` (N), `site_prev` (B), `site_go` (F) and `site_waypoint` (G). Driving adds `vehicle_enter` (F, gamepad Y; on foot it only takes the press with a car in reach, so Y still opens the map elsewhere), `drive_throttle` (right trigger) and `drive_brake` (left trigger); on the keyboard the move keys drive. The phone adds `phone_toggle` (B); while it is open it reads 1-6, the mouse buttons and the wheel itself. The AI activity feed adds `ai_feed` (J, keyboard only: every pad button is taken on foot) and reuses `site_waypoint` (G) on foot to route to the newest decision; the pad's Y keeps opening the map. Body actions add `act_strike` (left mouse, right trigger), `act_shove` (R, keyboard only), `act_aim` (right mouse, left trigger; on foot only, as the drive feature reads the triggers at the wheel) and `emote_wave`, `emote_yes`, `emote_no`, `emote_shout`, `emote_dance` (1-5, keyboard only; the open phone reads 1-6 itself). - `city_brain.gd` (AI activity): `stagger_mayors` (the activity feature sets it on foot, and teardown restores it). When it is on, `_on_day_started` queues the day's mayor turns in `turns` (`scripts/ai/mayor_turns.gd`, in an order that rotates daily) instead of running them at dawn. `_process` starts the k-th turn once `sim.day_progress()` reaches `MayorTurns.at(k)`, which spreads the turns over the first 90% of the day, and only while no sliced job or spread work is in flight (`MayorTurns.sim_idle`), so its actions never force a synchronous finish. `_on_day_ended` runs any turn still waiting; a mayor still deciding keeps the turn and takes it in `_day_gate` once free. `turns.next_turn()` reads the queue, and saves keep it under `turns` (`due` and `day`). Decision entries also carry `proposal` (the ordinance proposed this turn) and `votes` (the votes cast this turn). `GameBus.ping` reasons starting `ai:` are coloured by `hud_style.ping_color()` on the minimap and compass, and `FpHud` does not toast them. - `city_sim.gd` and `sim_defs.gd` (crews): `apply_player_order(mi, a)` is `apply_action` for the phone. The sites an order opens go to the player's `D.CREWS` crews instead of the contractors, and the order is kept in `crew_jobs` (`total` and `done` in crew seconds, `crew` -1 while it waits). `advance_crews(seconds)` moves the crews on. `_process` feeds it the frame time at game speed, holding time back while a sliced tick or spread work runs. Its sites go through their stages, finished orders open, and waiting orders take the crews that free up (`crew_job_started` / `crew_job_finished`, `cancelled` when a bulldoze clears the sites). `crew_schedule()`, `crew_wait()`, `crews_free()` and `crew_job_at(i)` answer the HUD, and saves keep the orders. `D.crew_seconds` / `D.build_crew_seconds` give the times: `CREW_MIN_S` (a park) to `CREW_MAX_S` (the airport) for buildings, `CREW_TILE_S` per road or bridge tile. - `construction_kit.gd` and `city_view.gd` (crews): `ConstructionKit.deck()` marks where people stand on a site's own parts: the ground among the rebar, the slab as poured, the slab a frame stands on (`SLAB_H`), every frame floor (each storey at least `FLOOR_H`) and the scaffold's boards, with the posts to keep clear. `decks_of()` reads the decks that show. The hoarding's gate faces the frontage road (else side `h % 4`) and is the `Site` root's `gate` meta. Each spoil berm blocks on its own, so the pit inside is open ground. `CityView.site_gate(i)` and `site_decks(i)` (world space) read them. @@ -109,11 +109,11 @@ Camera ownership: A owns every camera behaviour (follow, collision, V toggle, th | Slice | Owns (exclusive edit rights) | Delivers | |---|---|---| | **S0 Foundation** (serial, first) | `main.gd`, `project.godot` `[input]`, `scripts/fp/{fp_mode,fp_context,game_bus,fp_feature,fp_features,world_scale,asset_paths,city_directory}.gd`, the §4 seams, a stub file per slice, `tools/gate.sh`, `tools/tests/{parse_all,fp_smoke}*`, `tools/tests/fp_kit.gd` | On-foot boot with a basic walking camera. Clutter hidden. All seams. The gate. D13 hooks: camera settings and actions, the kit's first-person default. | -| **A Player & interaction** | `scripts/fp/player/**` (including `tps_rig.gd`, `spring_math.gd`, `player_avatar.gd`), `scripts/fp/world/collision_world.gd`, `scripts/fp/interact/**` | Kinematic controller: walk, run, crouch, jump, step onto curbs, head bob (first person), building and prop collision, water edge. **Third-person over-the-shoulder camera** (analytic spring arm, collision sweep, recenter, shoulder swap, zoom), **V toggle** to first person, the **hero avatar** (placeholder until HC, then HC's animated hero with head look), the **conversation two-shot**. Interaction with residents, buildings, posters and cars (raises `GameBus` events). Photo mode (free-fly). For Integration: body actions, the hero rig's action layer (`play_action`, `set_guard`, `strike_time`; `hero_fk.gd` measures the clips), `FpPlayer.aim_yaw` (the body squared to a direction) and `kick_view`, and `TpsRig.arm_scale`. | +| **A Player & interaction** | `scripts/fp/player/**` (including `tps_rig.gd`, `spring_math.gd`, `player_avatar.gd`), `scripts/fp/world/collision_world.gd`, `scripts/fp/interact/**` | Kinematic controller: walk, run, crouch, jump, step onto curbs, head bob (first person), building and prop collision, water edge. **Third-person over-the-shoulder camera** (analytic spring arm, collision sweep, recenter, shoulder swap, zoom), **V toggle** to first person, the **hero avatar** (placeholder until HC, then HC's animated hero with head look), the **conversation two-shot**. Interaction with residents, buildings, posters and cars (raises `GameBus` events). Photo mode (free-fly, held above the ground). For Integration: body actions, the hero rig's action layer (`play_action`, `set_guard`, `strike_time`; `hero_fk.gd` measures the clips), `FpPlayer.aim_yaw` (the body squared to a direction) and `kick_view`, and `TpsRig.arm_scale`. | | **B Lean HUD & map** | `scripts/fp/ui/**` except `chat_panel*` | Top indicator chips. Rotating circular minimap (roads, zones, water, player arrow, pings, incidents, nearby cars and people). Full-screen map (M). Compass. Street-name label. Toasts. Interact prompt anchored over its target in third person (crosshair only in first person). Pause menu (resume, save, load, settings for sensitivity, FOV, bob, quality preset and camera, quit to orbit, quit). Time-speed display. No model branding. | | **C Chat & conversations** | `scripts/fp/chat/**`, `scripts/ai/chatter_minds.gd`, `scripts/fp/ui/chat_panel*` | Clash of Clans–style global chat panel: collapsible, unread badge, role colours, district badges, relative time, system cards, a drip queue so lines don't burst. Chatter minds: residents and mayors talk about real events, gossip, reply to each other and to the player. Player input (Enter). Direct face-to-face conversations: E on a resident opens a dialogue that runs through that resident's mind. Answers in context: the reply and talk prompts carry the city's real state (`chat_facts.gd`: the mayor's last turn, the Gazette, the council, incidents, sites, district numbers), a mayor the player names answers in person, a typing row shows who is answering, and a status card says when live replies fail or come back. While nobody lives in the city yet, the rival mayors answer the player and argue among themselves once a minute (live only); a line nobody can answer gets "The mayors are busy fighting right now. Chat later." instead of a made-up reply. Face to face, a reply still out after 8 s gets the resident's busy line, and the late reply replaces it. Offline templates that quote the same facts. Save and load. Street lines (`street_lines.gd`, on `person_hit`): the one the visitor hit and a witness say so, and a crew called to the scene posts a City Alert card. | | **D Posters** | `scripts/ai/gmi_image_client.gd`, `scripts/fp/posters/**`, `tools/gmi/generate_posters.py`, `assets/posters/**` | Async HY image client (submit, poll, download, `user://` cache keyed by prompt hash). Poster director: mayor campaigns, Gazette front pages, shop ads using `CityDirectory` names, incident notices, ordinance notices. Surfaces: billboards, bus shelters, walls, hoardings at construction sites. Night lighting. The pre-baked offline set. | -| **E Traffic & pedestrians** | `scripts/fp/traffic/**`, `scripts/world/city_life.gd`, `scripts/world/resident_avatars.gd` | One road-agent system. Residents and ambient walkers drawn with HC's roster bodies and shared animation libraries in four LOD tiers (legacy static people until HC lands). Lane graph (right-hand traffic, turn arcs). IDM car-following. Intersections with traffic lights (4-way) and yield (T). Resident drivers whose trips come from their minds (`trip_started`). Ambient filler traffic and pedestrians on sidewalks. Buses, trucks and emergency crews on lanes. Agents yield to the player. Spinning wheels where the model allows. Cars the player's car hits are knocked (`shoves.gd`; `bodies_near` lists the vehicle bodies for the drive feature and `bump_car` takes its impulses): they slide and spin off and stop against walls and other cars. A lane car brakes hard, waits longer the harder it was hit, then steers back onto its lane and drives on, and lane traffic stops for it meanwhile. A parked car knocked out of its slot rides up curbs, stays where it stops and is towed back to its slot once it has stood still 30 s and is out of sight. Each car model's driver's door is cut out of its body between its shut lines and hung on its hinge (`car_doors.gd`), so the driven car's door swings open (`door_info`, `set_door`). People on foot take blows (`walkers_near`, `hit_walker`; Integration: body actions): `reactions.gd` makes them reel, fall and get up, then shout, run off or walk back, with the people around reacting too, and `incidents.gd` puts it in the residents' memories and moods and calls the police or an ambulance (`call_crew`, a `fleets.gd` crew that stays at the scene). `person_hit` on the bus reports each blow, and `answer_emote` has the people around answer the hero's emotes. Keeps `vehicle_counts()`, `visible_count()`, `pick()`, `position_of()` and `select()`. | +| **E Traffic & pedestrians** | `scripts/fp/traffic/**`, `scripts/world/city_life.gd`, `scripts/world/resident_avatars.gd` | One road-agent system. Residents and ambient walkers drawn with HC's roster bodies and shared animation libraries in four LOD tiers (legacy static people until HC lands). Lane graph (right-hand traffic, turn arcs). IDM car-following. Intersections with traffic lights (4-way) and yield (T). Resident drivers whose trips come from their minds (`trip_started`). Ambient filler traffic and pedestrians on sidewalks. Buses, trucks and emergency crews on lanes. Boats keep to open water (a bridge tile is not water to them), turn back where a bridge stands ahead and float with their waterline at the water surface. Agents yield to the player. Spinning wheels where the model allows. Cars the player's car hits are knocked (`shoves.gd`; `bodies_near` lists the vehicle bodies for the drive feature and `bump_car` takes its impulses): they slide and spin off and stop against walls and other cars. A lane car brakes hard, waits longer the harder it was hit, then steers back onto its lane and drives on, and lane traffic stops for it meanwhile. A parked car knocked out of its slot rides up curbs, stays where it stops and is towed back to its slot once it has stood still 30 s and is out of sight. Each car model's driver's door is cut out of its body between its shut lines and hung on its hinge (`car_doors.gd`), so the driven car's door swings open (`door_info`, `set_door`). People on foot take blows (`walkers_near`, `hit_walker`; Integration: body actions): `reactions.gd` makes them reel, fall and get up, then shout, run off or walk back, with the people around reacting too, and `incidents.gd` puts it in the residents' memories and moods and calls the police or an ambulance (`call_crew`, a `fleets.gd` crew that stays at the scene). `person_hit` on the bus reports each blow, and `answer_emote` has the people around answer the hero's emotes. Keeps `vehicle_counts()`, `visible_count()`, `pick()`, `position_of()` and `select()`. | | **F Sim performance** | `scripts/sim/city_sim.gd`, `scripts/sim/residents.gd`, `scripts/ai/resident_minds.gd` (perf only) | Time-sliced tick (frame budget ≤ 4 ms in first person, synchronous path kept for tests). A `tick_progress` signal. No stall over 50 ms. | | **G1 Streetscape & look** | `scripts/world/city_view.gd`, `scripts/world/construction_kit.gd`, `scripts/world/sky_director.gd`, `scenes/main-environment.tres`, `shaders/**`, `textures/real/**` and `env/**` (contents and `.import` params) | Scale fixes (REAL_SCALE, people, vehicles, machines via `WS`). Street-level ground: anti-tiling PBR, curbs, lane markings, crosswalks, sidewalk detail. Street furniture density. Street-name signs at intersections. First-person light profile (shadow distance, SSAO and SSIL radii in units, fog, volumetric fog for night and smog). Weather (clear, overcast, rain with wet roads). A budgeted view-rebuild queue. | | **G2 Render performance** | `scripts/fp/render/**`, `project.godot` `[rendering]`, `tools/optimize_imports.py`, the `.import` params under `models/**` **except `models/characters/**`** (HC) | Visibility ranges, HLOD and impostors, occluders (applied by a post-pass on `tile_built`). Quality presets (Low, Medium, High) applied at runtime: FSR2, shadow atlas, SSAO, SSIL. VRAM texture compression and mipmaps on imports (an allow-list keeps HC's retarget settings). `fp_bench` perf gate, including a third-person street shot and the animated-people budget. | @@ -121,12 +121,12 @@ Camera ownership: A owns every camera behaviour (follow, collision, V toggle, th | **HC Characters** (D13) | `tools/characters/**`, `models/characters/**` **including its `.import` files** (the BoneMap retarget settings are content; exception to rule 11), the character entries of `scripts/fp/asset_paths.gd` (`CHARACTERS_DIR`, `CHAR_BODIES_DIR`, `CHAR_ROSTER`, `CHAR_LIB_M`, `CHAR_LIB_F`, `HERO_M`, `HERO_F`, `PERSON_TPS_ANIMS`), `tools/tests/fp_characters_test.gd` | Realistic clothed people from Microsoft Rocketbox (MIT): 2 hero bodies and 38 resident bodies plus far LODs, on Godot's humanoid skeleton. Mocap clips (Rocketbox, plus Quaternius UAL CC0 for jump, crouch, interact, drive, carry and the body actions' punches, hit reactions, getting up, dance and shout) in one shared animation library per gender. `roster.json` with job pools and name genders. Licences and provenance. | | **I Audio** | `scripts/fp/audio/**`, `sounds/fp/**`, `scripts/audio_director.gd` | 3D positional engines, horns and sirens, footsteps by surface (in step with the hero's stride when the avatar animates), area ambience (traffic hum, park birds, river, construction), a UI chat blip, crashes where the player's car hits a wall or a car (`car_impact`, heavier and louder with the closing speed) a clatter or clang by kind for each street prop it knocks over (`prop_knocked`), and its driver's door opening and slamming shut (`car_door`), the thump of a blow on someone in the street and the fall of someone knocked down (`person_hit`), and a whoosh for each of the hero's swings (`hero_action`). Assets synthesized procedurally or with the existing ACE-Step tool. | | **Integration: city overview** | `scripts/fp/overview/**`, `tools/tests/fp_overview_test.gd` | Tab on foot: an aerial camera over the city (through `FpPlayer.set_view_override`, so `player.camera` stays the one camera) with a marker per open construction site and a site list. N / B pick sites, F walks the hero to a vantage point by the site, G sets a waypoint, Tab or Esc return. "Planner view" leaves for orbit; Tab in orbit comes back to where the hero stood (`Main._fp_saved`). | -| **Integration: driving** | `scripts/fp/drive/**`, `tools/tests/fp_drive_test.gd`, `tools/tests/fp_car_doors_test.gd` | F by a car takes it over (its resident's trip ends; traffic hands over a `player`-role car of the same model that the AI no longer steps). With the `car_doors` flag the hero gets in the long way (`car_entry.gd`): it walks round the car to the driver's door (the shorter clear way round its front or rear, past other cars, jogging while far), reaches for the handle (`door_try`), pulls the door open and swings it wide, steps into the opening, sits onto the sill (`sit_down`, its root travel walked back so the feet stay planted), swings round into the seat, and the door slams (`car_door` on the bus). From the reach on, a camera beside the door watches and blends into the chase camera. F on the way to the door turns back. An arcade bicycle model (`drive_model.gd`: throttle, brake, reverse, speed-scaled steering, grip by surface, handbrake slide) moves it through `TrafficFeature.drive_car`; the hero sits in the driver's seat. Chase camera (sweep-checked) or driver view on V, a speedometer and gear chip. The car is a rigid box (`car_body.gd`, on the shared `scripts/fp/obb2d.gd` and `scripts/fp/body_contacts.gd`): it moves in short substeps and each time resolves its deepest contact, with `CollisionWorld`'s walls, posts and water edges, with other vehicles (traffic's `bodies_near`, impulses handed back through `bump_car`) and with street props (the props feature knocks them over when hit hard enough). So it bounces a little off what it hits head-on, slides along what it hits at a glance and spins when hit off-centre; `car_impact` on the bus reports the hard hits. It strikes the people on foot it closes on faster than 0.5 m/s (traffic's `walkers_near` and `hit_walker`, as hard as the closing speed over 12 m/s, each at most once per 1.5 s), and they barely slow it. AI cars and walkers yield to player cars as oriented boxes. F again gets out on the driver's side when slow (under 8.5 m/s) and there is room: the long way, the car brakes to a stop, the door opens, the hero swings out onto the sill, stands up (`stand_up`), steps clear and the door shuts. With no room for the door or no clear way round (or without the flag) F gets in and out at once. A left car is removed once out of range and out of sight. | +| **Integration: driving** | `scripts/fp/drive/**`, `tools/tests/fp_drive_test.gd`, `tools/tests/fp_car_doors_test.gd` | F by a car takes it over (its resident's trip ends; traffic hands over a `player`-role car of the same model that the AI no longer steps). With the `car_doors` flag the hero gets in the long way (`car_entry.gd`): it walks round the car to the driver's door (the shorter clear way round its front or rear, past other cars, jogging while far), reaches for the handle (`door_try`), pulls the door open and swings it wide, steps into the opening, sits onto the sill (`sit_down`, its root travel walked back so the feet stay planted), swings round into the seat, and the door slams (`car_door` on the bus). From the reach on, a camera beside the door watches and blends into the chase camera. F on the way to the door turns back. An arcade bicycle model (`drive_model.gd`: throttle, brake, reverse, speed-scaled steering, grip by surface, handbrake slide) moves it through `TrafficFeature.drive_car`; the hero sits in the driver's seat. Chase camera (sweep-checked, and kept above the ground when it looks up from low down) or driver view on V, a speedometer and gear chip. The car is a rigid box (`car_body.gd`, on the shared `scripts/fp/obb2d.gd` and `scripts/fp/body_contacts.gd`): it moves in short substeps and each time resolves its deepest contact, with `CollisionWorld`'s walls, posts and water edges, with other vehicles (traffic's `bodies_near`, impulses handed back through `bump_car`) and with street props (the props feature knocks them over when hit hard enough). So it bounces a little off what it hits head-on, slides along what it hits at a glance and spins when hit off-centre; `car_impact` on the bus reports the hard hits. It strikes the people on foot it closes on faster than 0.5 m/s (traffic's `walkers_near` and `hit_walker`, as hard as the closing speed over 12 m/s, each at most once per 1.5 s), and they barely slow it. AI cars and walkers yield to player cars as oriented boxes. F again gets out on the driver's side when slow (under 8.5 m/s) and there is room: the long way, the car brakes to a stop, the door opens, the hero swings out onto the sill, stands up (`stand_up`), steps clear and the door shuts. With no room for the door or no clear way round (or without the flag) F gets in and out at once. A left car is removed once out of range and out of sight. | | **Integration: phone** | `scripts/fp/phone/**`, `tools/tests/fp_phone_test.gd` | B opens a phone with a Build app: road, homes, shops, industry, a building from the service list, and bulldoze, on the tile ahead of the hero (the camera's pitch sets the distance, 0.6 to 6 tiles). Each order goes through `CitySim.apply_player_order` for the mayor who owns that tile, so it is paid from that district's treasury and follows the mayors' rules. Its construction sites go to the player's crews (below; with the `crews` flag off, `apply_action` hands them to the contractors). Unclaimed land is refused. A line on the phone says how many crews are free and how long the order would take, or when a crew frees up for it. `build_rules.gd` mirrors the sim's checks for the preview: a cyan or amber ghost over the tiles, the owner, the price and why not. Roads and zones take two clicks (start or corner, then end). Put away while driving or under a menu. | | **Integration: AI activity** | `scripts/fp/activity/**`, `tools/tests/fp_activity_test.gd` | Shows the city's AI at work from the street. Every mayor turn, council ruling, Chaos Director pick, Grid Operator order and Gazette front page lands as a card in a feed at the top right (J cycles full, brief and hidden). A card names who decided and the model they run on, and says whether the answer came live, from a replay, from a fallback after a failed live call, or from the offline planner. It also shows what they did and why, any votes, and where. A decision with a place raises a light beacon in the agent's colour (tagged within 30 tiles) and pings the compass and minimap (`ai:`, no toast). G routes the GPS to the newest decision with a place. On foot it turns on `CityBrain.stagger_mayors`, so the five mayors take their turns spread over the day and the header line says who is deciding or who goes next. The feed hides under modals (not under the chat input), and the beacons hide in photo mode. | | **Integration: crews** | `scripts/fp/crews/**`, `tools/tests/fp_crews_test.gd`, `tools/tests/fp_crew_jobs_test.gd` | Phone orders go to the player's two building crews, which build them in real time: 2 to 5 minutes for a building at speed 1, roads by the tile. A third order waits for a free crew. Each crew is on its site to see (`crew_rig.gd`): a foreman by the gate, two builders whose clips follow the stage, and a hauler carrying crates from a pile to the work. They use the trades bodies from HC's roster (`crew_bodies.gd`, loaded in the background), with dust while they work and a board over the gate naming the crew, the order, the stage and the time left. Where they stand and walk comes from the site's `CrewGrid` (`crew_grid.gd`). It is a grid of cells 0.8 m apart with nodes on the ground inside the hoarding and on the kit's decks (on a narrow scaffold board, along its middle). Each node is clear of every collision box and cylinder near the tile, except the boxes that enclose its deck (the slab under it, the frame or scaffold round it), and of the deck's posts and of the decks above. Nodes link by steps up to 1 m and climbs over berms and formwork. `crew_spots.gd` picks the door inside the gate, the foreman's spot and the pile beside it, and work spots nearest the middle that the door reaches. It also picks a drop point the hauler reaches without walking into the crew. Where a finished building fills its lot, the crew works from the scaffold. A crew that takes an order while you play walks in through the gate. The crew is laid out again when its site is redrawn for a new stage (a worker walks over when it can), and a road crew moves along the road. A card under the top bar lists the orders with their stage, progress and countdown, and the ones waiting. E on a worker gets a word about the job, the site's interact label says which crew builds it, and a toast says when an order is done. | | **Integration: props** | `scripts/fp/props/**`, `tools/tests/fp_props_test.gd` | Street props the player's car knocks over. A bin, bench, planter, mailbox, bike rack or parking meter hit hard enough for its kind (`KINDS`: its mass, and the slowest the hit may send it flying for it to give way) falls: CityView knocks the batched prop down (not drawn, its collider marked `down`) and a copy of its meshes tumbles off (`tumble.gd`: it flies, tips over about one of its level axes, lands, rolls over its edges, slides and comes to rest on a face) over the walker's ground and off its walls. A fallen prop can be hit again. Slower hits, and lamps, trees, bollards, hydrants, utility boxes and signs, hold like walls. At most 24 lie fallen at once. A fallen prop stands up in its place again once it has lain 20 s and neither it nor its place is in sight, with the player away and no car on its place, and at once when its tile is drawn again. `prop_knocked` on the bus reports every fall (I plays the clatter). | -| **Integration: body actions** | `scripts/fp/actions/**`, `tools/tests/fp_actions_test.gd`, `tools/tests/fp_chat_street_test.gd` | The hero's fists and gestures on foot (flag `actions`). Left click (right trigger) shoves whoever stands in front. With the guard up (right mouse or left trigger held: fists up, the body squared to where the camera looks, the camera arm at 80%) it throws a jab and a cross in turn, starting again from the jab a second after the last. A swing lands when the rig's fist is furthest out (`HeroRig.strike_time`, measured on the clip with `hero_fk.gd`) on the nearest person on their feet within 1.2 m in the cone ahead, through traffic's `hit_walker`, as hard as the swing (harder when running), and kicks the view. Keys 1-5 wave, nod, shake the head, shout and dance, and the people around answer (traffic's `answer_emote`). Nothing swings in a car, in photo mode or the overview, in a conversation, under a modal or with the phone open, and the click that captures the mouse throws nothing. The presses come as events; a trigger counts once per pull. The person hit (traffic's `reactions.gd`) reels (`hit_chest`, `hit_head`) and slides back from a light blow. A blow of 0.7 or more, the third punch within 8 s or a fast car knocks them down (`knockback`): they lie a while, get up (`get_up`), then shake their head or shout at whoever did it, run off or walk back to where they were. Of the people within 8 m, up to 85% (by how bad it was) react: some run off, the rest shout or shake their heads. `incidents.gd` gives a resident who was hit a memory and a mood drop (a shove 0.5, a punch 1, a car 2, half as much again when knocked down), and those who saw it a memory and a drop of 0.3. A punch that knocks someone down, or the third within 60 s, calls the police; someone a car knocks down gets an ambulance. The crew (`call_crew`, a `fleets.gd` crew) drives out from the road by the nearest police station or hospital (from a road up to 10 tiles off when the city has none), stays at the scene a while, and each kind comes at most once per 90 s. `person_hit` on the bus reports every blow: C's `street_lines.gd` posts the victim's line and a witness's (one scene per 8 s of feed time; a harder blow still posts) and a City Alert card for a crew, and I plays the blow and the fall. `hero_action` reports each swing (I's whoosh) and emote. The car the player drives strikes people too (`car_body.gd`, cause `"car"`). | +| **Integration: body actions** | `scripts/fp/actions/**`, `tools/tests/fp_actions_test.gd`, `tools/tests/fp_chat_street_test.gd` | The hero's fists and gestures on foot (flag `actions`). Left click (right trigger) throws a jab and a cross in turn, starting again from the jab a second after the last, and R shoves. Each swing turns the body to whoever it goes for, else to where the camera looks. The guard (right mouse or left trigger held: fists up, the body squared to where the camera looks, the camera arm at 80%) is the fighting stance. A swing lands when the rig's fist is furthest out (`HeroRig.strike_time`, measured on the clip with `hero_fk.gd`) on the nearest person on their feet within 1.2 m in the cone ahead, through traffic's `hit_walker`, as hard as the swing (harder when running), and kicks the view. Keys 1-5 wave, nod, shake the head, shout and dance, and the people around answer (traffic's `answer_emote`). Nothing swings in a car, in photo mode or the overview, in a conversation, under a modal or with the phone open, and the click that captures the mouse throws nothing. The presses come as events; a trigger counts once per pull. The person hit (traffic's `reactions.gd`) reels (`hit_chest`, `hit_head`) and slides back from a light blow. A blow of 0.7 or more, the third punch within 8 s or a fast car knocks them down (`knockback`): they lie a while, get up (`get_up`), then shake their head or shout at whoever did it, run off or walk back to where they were. Of the people within 8 m, up to 85% (by how bad it was) react: some run off, the rest shout or shake their heads. `incidents.gd` gives a resident who was hit a memory and a mood drop (a shove 0.5, a punch 1, a car 2, half as much again when knocked down), and those who saw it a memory and a drop of 0.3. A punch that knocks someone down, or the third within 60 s, calls the police; someone a car knocks down gets an ambulance. The crew (`call_crew`, a `fleets.gd` crew) drives out from the road by the nearest police station or hospital (from a road up to 10 tiles off when the city has none), stays at the scene a while, and each kind comes at most once per 90 s. `person_hit` on the bus reports every blow: C's `street_lines.gd` posts the victim's line and a witness's (one scene per 8 s of feed time; a harder blow still posts) and a City Alert card for a crew, and I plays the blow and the fall. `hero_action` reports each swing (I's whoosh) and emote. The car the player drives strikes people too (`car_body.gd`, cause `"car"`). | | **Integration: reporter** | `scripts/fp/reporter/**`, `tools/tests/fp_reporter_test.gd` | The player as the Gazette's stringer (flag `reporter`): a goal on foot that follows the city's AI. `reporter_desk.gd` turns AI decisions into leads: a mayor turn with a place (`mayor_decided`: its focus tile and the first order carried out, "Viktor Han ordered a Police Station"), a council ruling that changed an ordinance (at City Hall) and a Chaos Director strike. At most 4 are open, newest first, and a lead goes cold a sim day after it happened. K takes the newest on assignment and sets the GPS waypoint to the far sidewalk across its street (the overview feature's `stand_point`). A card above the district caption (`reporter_card.gd`) shows the lead or the story: what happened, where and how far, the scoop clock, the quote so far and a checklist (the scene, a quote, an optional photo). Within 3 tiles of the scene, a reply face to face or the "Ask someone here" answer (`talk_replied`, emitted by C) is the quote: 12 characters or more, clipped to 140. A resident who lives in the deciding mayor's district is a local voice, and a passer-by never replaces one. In photo mode (`reporter_viewfinder.gd`) a click from within 4 tiles (80 m) with the scene inside the central 65% of the view is a sharp shot; the hero must have stood at the scene when the camera came up. K files the story: 100 points, +50 for a scoop (within a third of a day), +25 for a local voice and +25 for a sharp shot, towards the press ranks (Stringer, Reporter at 300, Senior reporter at 1000, Star reporter at 2500). The resident quoted remembers it, and `story_filed` has C run it as a Gazette line and a chatter topic. L opens the mayors' league (`reporter_league.gd`, modal `league`): the five mayors ranked on people, jobs, approval (what the district's residents think of their mayor) and funds, 25 points each, with their model's badge; then the press card, the story on assignment, the open leads with a Chase button each, and the latest stories. Points, stories filed and the first-lead tip are saved; leads and the assignment belong to the city on screen and are dropped on load and New City. | ## 6. Rules for every slice diff --git a/docs/fp/slices/A-player.md b/docs/fp/slices/A-player.md index 36a488ca..13c88d12 100644 --- a/docs/fp/slices/A-player.md +++ b/docs/fp/slices/A-player.md @@ -111,7 +111,7 @@ Write requests to `docs/fp/requests/A-needs.md`: - vehicle and pedestrian: from E's target label - poster: from D 4. **Photo mode** (`photo_mode` action, P, from `_unhandled_input`): - - Free-fly with no collision at `PHOTO_FLY_SPEED`, starting from the current camera transform (in third person, the arm's position). Down is `crouch` (Ctrl) and up is `jump` (Space); Q (`shoulder_swap`), V (`view_toggle`), the wheel and C are ignored, and E is `interact`. On exit the previous camera mode resumes with a snapped rig. + - Free-fly at `PHOTO_FLY_SPEED` through walls, but never lower than `TpsRig.GROUND_CLEAR` above the ground (the water surface off the land), starting from the current camera transform (in third person, the arm's position). Down is `crouch` (Ctrl) and up is `jump` (Space); Q (`shoulder_swap`), V (`view_toggle`), the wheel and C are ignored, and E is `interact`. On exit the previous camera mode resumes with a snapped rig. - In photo mode A ignores `interact`; FpMode ignores `hud_toggle` (F1) and C ignores `chat_open`. A sets S0's `FpPlayer.photo_mode` flag, which they read. - Hides the HUD (`hud.set_hud_visible(false)`) and pushes no modal, so the mouse stays captured. - The player position is saved on entry and restored on exit. @@ -244,7 +244,7 @@ A1 merges like any slice (ARCHITECTURE §7) and must pass without `models/charac - **Water.** Walking toward a water tile stops at the edge: `ground_height` stays finite and the feet never end up over a water tile. - **Props.** On a lit straight road tile, the pole derived from `Bulb_0` blocks the player (the feet never come within `PLAYER_RADIUS + 0.008` of it). A bus-stop tile from `view.bus_stops` blocks at the shelter. A holder with a test-injected `colliders` meta uses only that meta. - **Interact.** Facing a building within `INTERACT_RANGE` produces exactly one `interact_target_changed` with kind `building` and a non-empty label. A fake resident, injected through a stub feature's `pick()` at 2 m, wins over a building 5 m away. A stub `pick()` returning kind `pedestrian` with `data.driver_id` is delivered as kind `resident` with that id. Pressing interact — `Input.parse_input_event(InputEventAction{action = "interact", pressed = true})`, `Input.flush_buffered_events()`, one frame — emits `interact_requested`, and `talk_requested` for residents. While modal (`ctx.push_modal(&"test")`), `simulate` moves nothing and interact does nothing; pushing a modal emits `interact_target_changed({})`; an interact event in the same frame as `pop_modal` does nothing. -- **Photo mode.** Entering and exiting restores the position and the HUD visibility. Crouch moves down and jump moves up; interact does nothing while in photo mode. +- **Photo mode.** Entering and exiting restores the position and the HUD visibility. Crouch moves down and jump moves up, and flying down stops 0.20 m above the road; interact does nothing while in photo mode. - **Save.** `to_dict` / `from_dict` round-trips, including crouch. **Third person: `tools/tests/fp_tps_test.gd`**, token `FP_TPS_TEST: PASS`, built on `fp_kit.make_ctx(root, {"view": true, "grow": true, "camera_mode": "third"})`: diff --git a/project.godot b/project.godot index b579efe9..aeacfd03 100644 --- a/project.godot +++ b/project.godot @@ -276,6 +276,11 @@ act_strike={ , Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":5,"axis_value":1.0,"script":null) ] } +act_shove={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null) +] +} act_aim={ "deadzone": 0.5, "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null) diff --git a/scripts/fp/actions/actions_feature.gd b/scripts/fp/actions/actions_feature.gd index 71f7bce6..bcb13104 100644 --- a/scripts/fp/actions/actions_feature.gd +++ b/scripts/fp/actions/actions_feature.gd @@ -1,12 +1,13 @@ extends FpFeature ## Body actions (integration slice; docs/fp/ARCHITECTURE.md, Body actions): the hero's fists and -## gestures on foot. The strike button (left mouse, right trigger) shoves whoever stands in front; -## with the guard up (right mouse or left trigger held: fists up, the body squared to where the -## camera looks, the camera in closer) it throws a jab and a cross in turn instead. A blow lands -## when the rig's fist is furthest out (HeroRig.strike_time) on the person nearest in front within -## REACH, through the traffic feature's hit_walker: they reel or go down, the people around react -## and the city hears of it. Keys 1-5 wave, nod, shake the head, shout and dance, and the people -## around answer (traffic answer_emote). The bus hears every swing and emote (hero_action). +## gestures on foot. The strike button (left mouse, right trigger) throws a jab and a cross in +## turn, the body turning to whoever it goes for (else to where the camera looks); R shoves. The +## guard (right mouse or left trigger held: fists up, the body squared to where the camera looks, +## the camera in closer) is the fighting stance. A blow lands when the rig's fist is furthest out +## (HeroRig.strike_time) on the person nearest in front within REACH, through the traffic +## feature's hit_walker: they reel or go down, the people around react and the city hears of it. +## Keys 1-5 wave, nod, shake the head, shout and dance, and the people around answer (traffic +## answer_emote). The bus hears every swing and emote (hero_action). const WS := preload("res://scripts/fp/world_scale.gd") @@ -26,7 +27,14 @@ const EMOTES := { "emote_dance": ["dance", 8.0], } const INPUTS: Array[String] = [ - "act_strike", "act_aim", "emote_wave", "emote_yes", "emote_no", "emote_shout", "emote_dance" + "act_strike", + "act_shove", + "act_aim", + "emote_wave", + "emote_yes", + "emote_no", + "emote_shout", + "emote_dance", ] ## Who a swing can reach: on their feet within REACH (feet to feet) inside the cone ahead (the ## cosine); at contact still within REACH * HOLD_REACH. @@ -73,7 +81,8 @@ var _len: float = 0.0 var _hit_at: float = 0.0 var _landed: bool = true var _target: int = -1 -var _queued: bool = false +## The swing a press near the end of the last one queued ("punch", "shove") or "". +var _queued: String = "" var _combo_t: float = INF var _next_punch: String = "jab" var _emote_gap: float = 0.0 @@ -126,16 +135,18 @@ func can_act() -> bool: return phone == null or not bool(phone.get("open")) -## Starts a swing: a shove, or with the guard up the next punch of the combo. A press near the end -## of a swing queues the next. False when none starts now. -func strike() -> bool: - if not can_act(): +## Starts a swing: what "punch" throws the next punch of the combo (a jab, then a cross within +## COMBO_S of the last), "shove" a shove. A press near the end of a swing queues the next. False +## when none starts now. +func strike(what: String = "punch") -> bool: + if not can_act() or (what != "punch" and what != "shove"): return false if not swing.is_empty(): - _queued = _queued or _len - _t <= BUFFER_S + if _len - _t <= BUFFER_S: + _queued = what return false - var kind := "shove" - if guarding: + var kind := what + if what == "punch": kind = _next_punch if _combo_t < COMBO_S else "jab" _next_punch = "cross" if kind == "jab" else "jab" _combo_t = 0.0 @@ -154,10 +165,12 @@ func strike() -> bool: var feet := ctx.player.position var who := _find_target(_aim_dir()) _target = int(who.get("id", -1)) + # The body turns to whoever the swing goes for, else to where the camera looks. + _face_yaw = ctx.player.cam_yaw if not who.is_empty(): var d: Vector3 = (who["pos"] as Vector3) - feet _face_yaw = atan2(-d.x, -d.z) - _face_t = maxf(_len, FACE_S) + _face_t = maxf(_len, FACE_S) swings += 1 if ctx.bus != null: ctx.bus.hero_action.emit(kind, feet) @@ -200,7 +213,7 @@ func emote(e: String) -> bool: ## Ends the swing, the guard and any action on the rig, and gives the body and camera back. func cancel() -> void: swing = "" - _queued = false + _queued = "" _landed = true _target = -1 _forced_guard = false @@ -243,6 +256,8 @@ func _unhandled_input(event: InputEvent) -> void: return if press == "act_strike": strike() + elif press == "act_shove": + strike("shove") else: emote(str(EMOTES[press][0])) if is_inside_tree(): @@ -286,9 +301,10 @@ func _advance(dt: float) -> void: _land() if _t >= _len: swing = "" - if _queued: - _queued = false - strike() + if not _queued.is_empty(): + var next := _queued + _queued = "" + strike(next) ## The blow lands on the swing's target if they are still in reach and on their feet. diff --git a/scripts/fp/drive/drive_feature.gd b/scripts/fp/drive/drive_feature.gd index ddfcf621..d9fb1938 100644 --- a/scripts/fp/drive/drive_feature.gd +++ b/scripts/fp/drive/drive_feature.gd @@ -534,7 +534,8 @@ func _look(dt: float) -> void: _orbit_pitch = move_toward(_orbit_pitch, 0.0, ORBIT_RETURN * 0.5 * dt) -## The camera now: behind and above the car (pulled in front of walls), or at the driver's eyes. +## The camera now: behind and above the car (pulled in front of walls, lifted off the ground when +## it looks up from low down), or at the driver's eyes. func camera_xform() -> Transform3D: if view == "driver" and _seat != null: var car := Transform3D(Basis(Vector3.UP, model.yaw), _pos) @@ -555,10 +556,17 @@ func camera_xform() -> Transform3D: CHASE_DIST ) var eye := target + dir * dist + eye.y = maxf(eye.y, _ground_below(eye) + CAM_RADIUS) var aim := target + Vector3(sin(y), 0.0, cos(y)) * LOOK_AHEAD return Transform3D(Basis.looking_at(aim - eye, Vector3.UP), eye) +## The ground under p (the water surface off the land and beside a bridge deck). +func _ground_below(p: Vector3) -> float: + var g := ctx.collision.ground_height(p) if ctx.collision != null else WS.GROUND_Y + return WS.WATER_Y if is_nan(g) else g + + func _fov() -> float: var base := float(ctx.settings.get("fov", WS.FOV)) return base + FOV_FAST * clampf(absf(model.speed()) / MODEL.TOP, 0.0, 1.0) diff --git a/scripts/fp/player/player.gd b/scripts/fp/player/player.gd index d5340dad..69ac5fa8 100644 --- a/scripts/fp/player/player.gd +++ b/scripts/fp/player/player.gd @@ -985,6 +985,10 @@ func _photo_fly(wish: Vector2, sprint: bool, down: bool, up: bool, delta: float) if v.length_squared() > 1.0: v = v.normalized() _photo_pos += v * WS.PHOTO_FLY_SPEED * (3.0 if sprint else 1.0) * delta + # The ground stops the camera: it never flies under the road. + if ctx != null and ctx.collision != null: + var g := ctx.collision.ground_height(_photo_pos) + _photo_pos.y = maxf(_photo_pos.y, (WS.WATER_Y if is_nan(g) else g) + TpsRig.GROUND_CLEAR) position = _photo_pos - Vector3.UP * WS.EYE _update_tile() diff --git a/scripts/fp/ui/pause_menu.gd b/scripts/fp/ui/pause_menu.gd index 4a64d613..e444ffa6 100644 --- a/scripts/fp/ui/pause_menu.gd +++ b/scripts/fp/ui/pause_menu.gd @@ -55,7 +55,7 @@ const LEGEND := [ ] const KEYS := [ ["M", "Map"], ["Tab", "City overview"], ["V", "Camera view"], ["Q", "Shoulder"], ["E", "Interact"], ["F", "Get in / out of a car"], ["B", "Build phone"], - ["Click", "Shove"], ["Right-click", "Guard (click to punch)"], ["1-5", "Emotes"], + ["Click", "Punch"], ["Right-click", "Guard"], ["R", "Shove"], ["1-5", "Emotes"], ["Enter", "Chat"], ["J", "AI activity"], ["G", "Route to AI decision"], ["K", "Take lead / file story"], ["L", "Mayors' league"], ["[ ]", "Game speed"], ["T", "Skip to morning"], ["P", "Photo mode"], ["F1", "Hide HUD"], diff --git a/scripts/world/city_life.gd b/scripts/world/city_life.gd index 26882ab4..6c13542e 100644 --- a/scripts/world/city_life.gd +++ b/scripts/world/city_life.gd @@ -11,6 +11,8 @@ const WS := preload("res://scripts/fp/world_scale.gd") const M := "res://models/" const ROAD_SCALE := 0.17 const DIRS := [Vector2i(0, -1), Vector2i(1, 0), Vector2i(0, 1), Vector2i(-1, 0)] +## How far boats bob above and below the water surface (u). +const BOB := 0.006 ## Which crews answer each disaster type; the first entry is the station kind they leave from. const RESPONSE := { "fire": [["firetruck", D.FIRE], ["firetruck", D.FIRE], ["ambulance", D.HOSPITAL]], @@ -65,7 +67,7 @@ func setup(p_sim: CitySim, p_view: Node3D) -> void: _air_root = _root("Aircraft") _water_tiles.clear() for i in sim.active: - if sim.terrain[i] == D.WATER: + if _open_water(i): _water_tiles.append(Vector2i(D.tx(i), D.ty(i))) for _b in range(clampi(_water_tiles.size() / 18, 0, 5)): _spawn_boat() @@ -463,11 +465,16 @@ func _spawn_boat() -> void: _boats.append(b) +## Water a boat can sail: no bridge over it (a road is the only thing built on water). +func _open_water(i: int) -> bool: + return sim.terrain[i] == D.WATER and sim.kind[i] != D.ROAD + + func _water_neighbors(p: Vector2i) -> Array[Vector2i]: var out: Array[Vector2i] = [] for dv: Vector2i in DIRS: var n := p + dv - if D.in_bounds(n.x, n.y) and sim.terrain[D.idx(n.x, n.y)] == D.WATER: + if D.in_bounds(n.x, n.y) and _open_water(D.idx(n.x, n.y)): out.append(n) return out @@ -485,9 +492,17 @@ func _update_boats(delta: float) -> void: b["to"] = nbrs[randi() % nbrs.size()] if not nbrs.is_empty() else cur var from: Vector2i = b["from"] var to: Vector2i = b["to"] + # A bridge built ahead of a boat turns it back. + if not _open_water(D.idx(to.x, to.y)) and _open_water(D.idx(from.x, from.y)): + b["from"] = to + b["to"] = from + b["t"] = 1.0 - float(b["t"]) + from = b["from"] + to = b["to"] var node: Node3D = b["node"] var p := Vector3(from.x, 0, from.y).lerp(Vector3(to.x, 0, to.y), float(b["t"])) - node.position = Vector3(p.x, 0.02 + sin(_time * 2.0 + float(b["phase"])) * 0.012, p.z) + # The models' origin is their waterline. + node.position = Vector3(p.x, WS.WATER_Y + sin(_time * 2.0 + float(b["phase"])) * BOB, p.z) var dir := Vector3(to.x - from.x, 0, to.y - from.y) if dir.length_squared() > 0.0: node.rotation.y = lerp_angle(node.rotation.y, atan2(dir.x, dir.z), minf(1.0, delta * 2.5)) diff --git a/tools/tests/fp_actions_test.gd b/tools/tests/fp_actions_test.gd index 760162ee..f1625d35 100644 --- a/tools/tests/fp_actions_test.gd +++ b/tools/tests/fp_actions_test.gd @@ -1,10 +1,11 @@ extends SceneTree ## Body actions (integration feature "actions"; docs/fp/ARCHITECTURE.md, Body actions): the hero -## rig's action layer (strike times, the guard, punches, the dance); the strike button shoving -## whoever stands in front, the guard turning it into a jab-cross combo whose third blow knocks -## them down, swings at nobody; emotes the people around answer; the guard squaring the body to -## the camera; what blocks the actions; presses (events, a trigger's edge) and held input; the -## player's car striking people (car_body.gd); person_hit and hero_action on the bus; teardown. +## rig's action layer (strike times, the guard, punches, the dance); the strike button punching +## whoever stands in front (the body turned to them, a press near the end queuing the next) in a +## jab-cross combo whose third blow knocks them down, R shoving, swings at nobody; emotes the +## people around answer; the guard squaring the body to the camera; what blocks the actions; +## presses (events, a trigger's edge) and held input; the player's car striking people +## (car_body.gd); person_hit and hero_action on the bus; teardown. ## Third person on a grown city; time runs through the feature's tick(1/60), traffic's step and ## the player's update_view. @@ -52,6 +53,7 @@ func _init() -> void: _check_inputs() if rig != null: _check_rig() + _check_punch() _check_shove() _check_combo() _check_miss() @@ -144,6 +146,7 @@ func _check_inputs() -> void: keys.append(_key(a)) var keys_ok := keys == [KEY_1, KEY_2, KEY_3, KEY_4, KEY_5] K.check(fails, keys_ok, "keys 1-5 wave, nod, shake the head, shout and dance (%s)" % str(keys)) + K.check(fails, _key("act_shove") == KEY_R, "R shoves (%d)" % _key("act_shove")) func _check_rig() -> void: @@ -181,12 +184,48 @@ func _check_rig() -> void: _step(1.0, false) +## With the guard down the strike button throws a punch at whoever stands in front, and the body +## turns to them; a press near the end of a swing queues the next punch. +func _check_punch() -> void: + var p := ctx.player + var w := _person(0.8) + hits.clear() + acts.clear() + var d := w.pos - p.position + var to_w := atan2(-d.x, -d.z) + p.yaw = wrapf(to_w + 0.6, -PI, PI) + p.cam_yaw = to_w + var started: bool = f.call("strike") == true and str(f.get("swing")) == "jab" + K.check(fails, started, "with the guard down the strike button punches (a jab)") + K.check(fails, f.call("strike", "kick") == false, "an unknown swing is refused") + _step(0.4) + K.check(fails, _causes() == ["punch"], "it lands on the person in front (%s)" % str(_causes())) + var info: Dictionary = hits[0][2] if not hits.is_empty() else {} + K.check( + fails, absf(float(info.get("strength", 0.0)) - 0.45) < 0.01, "a standing jab (%s)" % info + ) + var off := absf(angle_difference(p.yaw, to_w)) + K.check(fails, off < 0.2, "the body turns to them (%.2f rad off)" % off) + K.check(fails, acts == ["jab"], "hero_action hears the jab (%s)" % str(acts)) + _remove(w) + _step(1.1) + f.call("strike") + _step(0.3) + K.check(fails, f.call("strike") == false, "a press near the end of the swing is queued") + _step(0.15) + K.check(fails, str(f.get("swing")) == "cross", "and the cross follows (%s)" % f.get("swing")) + _step(0.7) + K.check(fails, str(f.get("swing")).is_empty(), "then the hands come down") + p.teleport(p.position, rad_to_deg(p.cam_yaw)) + _step(1.1) + + func _check_shove() -> void: var w := _person(0.8) hits.clear() acts.clear() - var started: bool = f.call("strike") == true and str(f.get("swing")) == "shove" - K.check(fails, started, "with the guard down the strike button shoves") + var started: bool = f.call("strike", "shove") == true and str(f.get("swing")) == "shove" + K.check(fails, started, 'strike("shove") shoves') K.check(fails, f.call("strike") == false, "one swing at a time") _step(0.1) K.check(fails, hits.is_empty(), "the shove lands once the arms are out, not at once") @@ -213,9 +252,6 @@ func _check_combo() -> void: var w := _person(0.8) hits.clear() acts.clear() - f.call("set_guard", true) - _step(0.3) - K.check(fails, bool(f.get("guarding")), "set_guard raises the guard") var downs: Array[bool] = [] for _k in range(3): _close_in(w, 0.8) @@ -224,7 +260,7 @@ func _check_combo() -> void: for h: Array in hits: downs.append(bool((h[2] as Dictionary).get("down", false))) var combo := acts == ["jab", "cross", "jab"] - K.check(fails, combo, "with the guard up the punches go jab, cross, jab (%s)" % str(acts)) + K.check(fails, combo, "the punches go jab, cross, jab (%s)" % str(acts)) K.check(fails, _causes() == ["punch", "punch", "punch"], "each lands (%s)" % str(_causes())) K.check( fails, downs == [false, false, true], "the third in a row knocks them down %s" % str(downs) @@ -234,8 +270,6 @@ func _check_combo() -> void: f.call("strike") _step(0.55) K.check(fails, hits.size() == 3, "nobody is punched lying down") - f.call("set_guard", false) - _step(0.3) _remove(w) _step(0.5) @@ -290,6 +324,7 @@ func _check_guard() -> void: f.call("set_guard", true) p.cam_yaw = wrapf(p.yaw + 1.0, -PI, PI) _step(1.0) + K.check(fails, bool(f.get("guarding")), "set_guard raises the guard") K.check( fails, is_equal_approx(p.aim_yaw, p.cam_yaw), @@ -364,6 +399,10 @@ func _check_held_input() -> void: K.check(fails, acts == ["yes"], "key 2 nods (%s)" % str(acts)) f.call("cancel") _step(1.1) + _send(K.key_event(KEY_R)) + K.check(fails, acts == ["yes", "shove"], "R shoves (%s)" % str(acts)) + f.call("cancel") + _step(1.1) Input.action_press("act_aim") _step(0.1) K.check(fails, bool(f.get("guarding")), "holding aim keeps the guard up") diff --git a/tools/tests/fp_boats_test.gd b/tools/tests/fp_boats_test.gd new file mode 100644 index 00000000..260299ec --- /dev/null +++ b/tools/tests/fp_boats_test.gd @@ -0,0 +1,109 @@ +extends SceneTree +## Boats on the river (scripts/world/city_life.gd): they sail open water only, never through a +## bridge, turn back where a bridge stands ahead of them, and float with their waterline at the +## water surface. A grown city with a river and its bridges; time runs through _update_boats. + +const K := preload("res://tools/tests/fp_kit.gd") +const D := preload("res://scripts/sim/sim_defs.gd") +const WS := preload("res://scripts/fp/world_scale.gd") +const DT := 0.1 +const STEPS := 3000 +const DIRS := [Vector2i(0, -1), Vector2i(1, 0), Vector2i(0, 1), Vector2i(-1, 0)] + +var fails: Array[String] = [] +var sim: CitySim +var life: CityLife +var bridges: Array[int] = [] + + +func _init() -> void: + await process_frame + var ctx := K.make_ctx(get_root(), {"view": true, "grow": true}) + sim = ctx.sim + life = ctx.view.life + var water := 0 + for i in sim.active: + if sim.terrain[i] == D.WATER: + water += 1 + if sim.kind[i] == D.ROAD: + bridges.append(i) + var fixture := "fixture: a river with bridges (%d tiles, %d bridges)" % [water, bridges.size()] + K.check(fails, water > 20 and not bridges.is_empty(), fixture) + var boats: Array = life.get("_boats") + K.check(fails, not boats.is_empty(), "boats sail the river (%d)" % boats.size()) + if bridges.is_empty() or boats.is_empty(): + K.finish(self, "FP_BOATS_TEST", fails) + return + _check_sailing(boats) + _check_turn_back(boats[0]) + K.finish(self, "FP_BOATS_TEST", fails) + + +func _bridge(t: Vector2i) -> bool: + return sim.kind[D.idx(t.x, t.y)] == D.ROAD + + +## STEPS steps of DT: every boat stays on open water at the surface and keeps moving. +func _check_sailing(boats: Array) -> void: + var on_bridge := 0 + var worst_y := 0.0 + var moves := 0 + var last: Array = boats.map(func(b: Dictionary) -> Vector2i: return b["from"]) + for _k in range(STEPS): + life.call("_update_boats", DT) + for j in range(boats.size()): + var b: Dictionary = boats[j] + if _bridge(b["from"]) or _bridge(b["to"]): + on_bridge += 1 + if b["from"] != last[j]: + moves += 1 + last[j] = b["from"] + var y := (b["node"] as Node3D).position.y + worst_y = maxf(worst_y, absf(y - WS.WATER_Y)) + K.check(fails, on_bridge == 0, "no boat sails through a bridge (%d steps on one)" % on_bridge) + K.check( + fails, + worst_y <= CityLife.BOB + 1e-6, + "boats float at the water surface (%.2f m off at most)" % (worst_y / WS.M) + ) + K.check(fails, moves >= boats.size() * 30, "and keep moving (%d tiles sailed)" % moves) + + +## A boat heading from open water into a bridge turns back where it is and sails on elsewhere. +func _check_turn_back(b: Dictionary) -> void: + var br := -1 + var open := Vector2i.ZERO + for i: int in bridges: + for dv: Vector2i in DIRS: + var n := Vector2i(D.tx(i), D.ty(i)) + dv + if br < 0 and D.in_bounds(n.x, n.y) and sim.terrain[D.idx(n.x, n.y)] == D.WATER: + if not _bridge(n): + br = i + open = n + K.check(fails, br >= 0, "fixture: open water beside a bridge") + if br < 0: + return + var bt := Vector2i(D.tx(br), D.ty(br)) + var nbrs: Array = life.call("_water_neighbors", open) + K.check(fails, not nbrs.has(bt), "the bridge is not a way on from beside it (%s)" % str(nbrs)) + b["from"] = open + b["to"] = bt + b["t"] = 0.3 + var want := Vector2(open).lerp(Vector2(bt), 0.3 + float(b["speed"]) * DT) + life.call("_update_boats", DT) + var node := b["node"] as Node3D + var at := Vector2(node.position.x, node.position.z) + var turned: bool = b["to"] == open and b["from"] == bt and at.distance_to(want) < 1e-4 + var turn_msg := "it turns back where it is (%s -> %s, %.4f off)" + K.check(fails, turned, turn_msg % [b["from"], b["to"], at.distance_to(want)]) + var turn_d := at.distance_to(Vector2(bt)) + var closest := INF + for _k in range(100): + life.call("_update_boats", DT) + at = Vector2(node.position.x, node.position.z) + closest = minf(closest, at.distance_to(Vector2(bt))) + K.check( + fails, + closest >= turn_d - 1e-4, + "and gets no closer to the bridge (%.2f tiles, %.2f at the turn)" % [closest, turn_d] + ) diff --git a/tools/tests/fp_boats_test.gd.uid b/tools/tests/fp_boats_test.gd.uid new file mode 100644 index 00000000..70a03ee4 --- /dev/null +++ b/tools/tests/fp_boats_test.gd.uid @@ -0,0 +1 @@ +uid://ddsjdf5vv16l7 diff --git a/tools/tests/fp_drive_test.gd b/tools/tests/fp_drive_test.gd index e52d2567..85274470 100644 --- a/tools/tests/fp_drive_test.gd +++ b/tools/tests/fp_drive_test.gd @@ -62,6 +62,7 @@ func _init() -> void: _check_enter(it) _check_seated() _check_drive() + _check_camera() _check_yield() _check_exit() _check_crash() @@ -325,6 +326,24 @@ func _check_drive() -> void: _drive(0.0, 1.0, 0.0, 2.0) +## The chase camera rides above the car; looking up from low down (the mouse pushed all the way) +## lowers it but never under the road. +func _check_camera() -> void: + var car := _car() + var high: Transform3D = drive.call("camera_xform") + var above := (high.origin.y - car.pos.y) / WS.M + K.check(fails, above > 1.0, "the chase camera rides above the car (%.2f m)" % above) + drive.call("_orbit_by", 0.0, -PI) + var low: Transform3D = drive.call("camera_xform") + var clear := (low.origin.y - ctx.collision.ground_height(low.origin)) / WS.M + K.check( + fails, + clear > 0.2 and low.origin.y < high.origin.y, + "looking up from low down it stays above the road (%.2f m)" % clear + ) + drive.set("_orbit_pitch", 0.0) + + func _check_yield() -> void: var car := _car() var vh := _vh() diff --git a/tools/tests/fp_player_test.gd b/tools/tests/fp_player_test.gd index dedc261f..c051df12 100644 --- a/tools/tests/fp_player_test.gd +++ b/tools/tests/fp_player_test.gd @@ -604,6 +604,13 @@ func _check_photo(ctx: FpContext) -> void: p.update_view(DT) var y2 := p.view_position().y K.check(fails, y1 < y0 - 0.1 * M and y2 > y1 + 0.5 * M, "crouch flies down (%.2f m) and jump up (%.2f m)" % [(y1 - y0) / M, (y2 - y1) / M]) + for _k in range(180): + p.simulate(Vector2.ZERO, false, DT, true, false) + p.update_view(DT) + var low := p.view_position() + var clear := (low.y - ctx.collision.ground_height(low)) / M + var stop_msg := "the ground stops the flight down (%.2f m above it)" % clear + K.check(fails, clear > 0.19 and clear < 0.21, stop_msg) if feat != null: feat.call("evaluate") _press("interact")