HardYards/web/world/js/player.sim.js
type-two 7758b65331 Lane D: the player leans into the wind (gate 2.4 — as a root pose, not a clip)
The QA pass's biggest player finding: 'stands bolt upright and unbothered'
at 65 km/h. E rendered the two Mixamo lean candidates and rejected both on
screen, and proved the deeper reason a clip can't do this: _rotOnly drops
Hips.quaternion from every clip at load, so a baked lean is discarded, and
a clip's lean axis is fixed while the wind's bearing swings. So the lean is
the ROOT, the same machine as the knockdown pitch and climbY lift.

sim (player.sim.js): sim.lean 0..1 + sim.leanDir, eased over leanSecs.
Magnitude off windBase (the ~4s EMA), NOT raw windSpeed — measured: raw
strobes the posture 8-18x/min across the storms, windBase settles it to 0-2
changes per 90s. Sustained wind = posture; the gust is already the
stumble/knock event. Suppressed on your back (pitch owns the body), braced
(TakeCover is its own hunch), and up a ladder. Bands land on the storm
rating ladder: night 1 upright, 2-3 lean, 4-5 in the gale.

view (player.js): a foot-pivot tip toward leanDir, max leanMaxRad (~17deg,
a third of a knockdown's pi/2), composed under the yaw. It's the else branch
of the pitch tip by construction — the sim guarantees lean=0 whenever
pitch>0, so they never fight.

Three asserts: magnitude scales with sustained speed, a lone gust does NOT
snap you over (windBase inertia), and it's downwind + suppressed when
floored/braced/aloft. Mutation-checked: forcing lean to 0 reddens two.
Verified live at 71 km/h — leanDir dot wind = 1.0, the figure visibly
braces. Selftest 340/0/0. This is the root lean E asked me to wire first;
whether their WindPosture additive follows is a look-at-it-together call.
2026-07-18 01:33:47 +10:00

432 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* player.sim.js — the small person's deterministic core. (Lane D)
*
* Zero imports, on purpose. PLAN3D §0 requires the sim to be fast-forwardable in selftest with a
* fixed dt and no renderer, so nothing in here touches THREE, the DOM, rAF, Date.now() or
* Math.random(). `step(dt, t, …)` is the entire clock: same inputs → same trace, every run.
*
* player.js owns the view (rig, clips, camera). This file owns the truth: where the person is,
* what state they're in, and what the wind is doing to them.
*/
/**
* The state machine, as a table (PLAN3D §5-D.5 asks for a table test).
* clip — clip name in player_anims.glb
* carryClip — clip to use instead when the player has something in their hands
* locked — movement input is ignored, and `player.busy` is true
* secs — timed states auto-advance to `next` after this long
* Invariant the selftest enforces: every locked state either has a `next` (so it drains on its own)
* or names an external releaser. `busy` and `shelter` are the released ones — interact.js and the
* shelter key each both ENTER and LEAVE their own state, so a dropped release can't strand you.
*/
export const STATES = {
idle: { clip: 'Idle', carryClip: 'CarryIdle', locked: false, loop: true },
walk: { clip: 'Walk', carryClip: 'Carry', locked: false, loop: true },
run: { clip: 'Run', carryClip: 'Carry', locked: false, loop: true },
busy: { clip: 'Idle', locked: true, loop: true, releasedBy: 'interact' },
shelter: { clip: 'TakeCover', locked: true, loop: true, releasedBy: 'input' },
stumble: { clip: 'StumbleBack', locked: true, loop: false, secs: 0.8, next: 'idle' },
stagger: { clip: 'Reaction', locked: true, loop: false, secs: 0.9, next: 'idle' },
knocked: { clip: 'Falling', locked: true, loop: false, secs: 1.4, next: 'getup' },
getup: { clip: 'CrouchToStand', locked: true, loop: false, secs: 1.3, next: 'idle' },
// --- the ladder (decision 12). climbY is driven in code and the clip plays on top, exactly the
// knockdown precedent: _rotOnly strips the root, so ClimbLadder can no more lift the body than
// Falling could lay it down. ladder.js calls climbTo(); the sim owns the height. ---
climb: { clip: 'ClimbLadder', locked: true, loop: true, releasedBy: 'ladder' },
// atTop is deliberately NOT locked: `busy` gates interact.js, and the whole point of being up
// there is that hold-E works. Movement is stopped by `onLadder` instead, not by `locked`.
atTop: { clip: 'Idle', carryClip: 'CarryIdle', locked: false, loop: true, releasedBy: 'ladder' },
};
/**
* True while the player is on a ladder — movement is off, and the wind is meaner.
*
* Keyed on HEIGHT, not on state, and that distinction is load-bearing: hold-E puts you into `busy`,
* so a state-based test would say you'd stepped off the ladder the instant you started the repair
* you climbed up to do. (It did exactly that — the gate cancelled its own hold.) Height is the
* physical truth and survives every state the ladder passes through.
*/
export const onLadder = (sim) => sim.climbY > 0.02 || sim.state === 'climb';
/**
* Which clip a state actually plays right now. Carrying swaps the locomotion set (Carry/CarryIdle),
* and an interaction can name its own verb (`Crank` at a turnbuckle, `PickUp` at the shed table) —
* interact.js writes that into `sim.busyClip`. Everything else is the table's `clip`.
* Kept here rather than in player.js so the selftest can assert it without a renderer.
*/
export function clipFor(sim) {
const st = STATES[sim.state];
if (sim.state === 'busy' && sim.busyClip) return sim.busyClip;
if (sim.carrying && st.carryClip) return st.carryClip;
return st.clip;
}
/**
* Tuning. Ported from the 2D prototype's shape (prototype/game.js:250-252), retuned to metres and
* m/s per PLAN3D §1 ("port the behaviour, retune the constants").
*/
export const TUNE = {
walkSpeed: 1.5, // m/s — a person crossing a 30 m yard, unhurried
runSpeed: 4.4, // m/s — shift
accel: 16, // m/s² toward the wanted velocity
turnRate: 11, // rad/s — facing chases the movement direction
// prototype: slow = 1 - min(0.35, wind.speed / 160). Its wind ran ~4 (calm) to ~38 (gust peak),
// which is already m/s-shaped, so the curve ports across directly.
slowMax: 0.35,
slowRef: 160,
// prototype: push = ws*ws*0.55 — "wind pressure goes with speed², gusts have teeth"; and shove
// only applied while wind.gust > 8, never from the base wind.
shoveK: 0.0035, // shove accel (m/s²) = shoveK · ws² → ~3.2 m/s² in a 30 m/s gust
// Gate RETUNED against the real storm_02 (see the header note): the prototype's literal 8 was a
// low bar in ITS units (gusts ran 1238, so 8 was "almost always"); ported straight across it
// became a high bar in ours (gusts peak at 12.1) and threw away most of the shove. 4 restores the
// prototype's intent — you're being pushed for ~26 s of a 90 s storm, and a calm day is still calm.
shoveGustMin: 4, // m/s of gust (over baseline) before the wind can push you at all
shoveDamp: 2.5, // 1/s foot-friction bleed → terminal drift ≈ shoveK·ws²/shoveDamp
// Baseline tracker: contracts.js exposes wind.sample() (total) and wind.gustTelegraph() (before
// the gust), but nothing reports gust magnitude DURING the hold. Rather than widen Lane C's
// contract, we recover it: a slow EMA of local wind speed is the base curve, and everything above
// it is gust. Self-calibrating to whatever storm JSON Lane C authors.
baseTrack: 0.25, // 1/s — ~4 s memory; gust holds are ~1.7 s, so they read as gust, not base
// Knockdown mirrors the sail's failure rule (PLAN3D §1: break after 0.4 s SUSTAINED overload) —
// same verb for cloth and for people, so the player reads one language.
knockWind: 30, // m/s sustained local wind → you go down
knockSustain: 0.5, // s above knockWind before it happens
knockBleed: 2, // exposure drains this many × faster than it fills
pitchSecs: 0.35, // s for the body to swing down / back up (view reads sim.pitch)
// Wind lean (SPRINT13 gate 2.4). The QA pass: "the player stands bolt upright and unbothered" at
// 65 km/h. E rendered the two Mixamo lean candidates and rejected both on screen (a couch-shove
// and a bus-stop slouch), and proved the deeper reason a clip can't do this at all: _rotOnly drops
// Hips.quaternion from every clip at load (player.js:44), so a baked lean is discarded — and a
// clip's lean axis is fixed while the wind's bearing swings. So the lean is the ROOT, same machine
// as the knockdown pitch: the sim owns an angle and a direction, the view tips the body by it.
// Keyed on windBase (the ~4 s EMA), NOT raw windSpeed — measured: raw strobes the posture 8-18×/min
// across the storms, windBase settles it to 0-2 changes per 90 s. Sustained wind = posture; gust is
// already the stumble/knock event. Bands land on the storm rating ladder (D's THREADS table): night
// 1 upright all night, 2-3 lean, 4-5 in the gale.
leanWindMin: 8.3, // m/s windBase (30 km/h — the speed C's leaves start at) before you lean at all
leanWindFull: 22, // m/s windBase for a full lean — past the wildnight median, shy of its 25 peak
leanMaxRad: 0.30, // rad (~17°) of torso tip at full lean. A brace-into-it, not a fall (pitch is 0.5π)
leanSecs: 0.6, // s to reach/leave the target lean — slow, so a lull visibly lets you straighten
// A gust that can't floor you can still break your stride. Sits BELOW knockWind on purpose, so a
// storm reads as: shoved → stumbling → floored, rather than fine-fine-fine-flat-on-your-back.
// RETUNED against the real storm_02: 17 was set against a mock that injected +18 gusts, but the
// real storm's gusts-over-baseline peak at 12.1, so StumbleBack was unreachable — dead code in the
// shipped game. 9 catches the top third of the 12 gusts in a wild night: 4 stumbles to 2
// knockdowns, which is the intended rhythm (stumble is the beat, knockdown the punchline).
// d.test.js pins this to the actual storm JSON so a reweighted downdraft can't quietly kill it again.
stumbleGust: 9, // m/s over baseline → you lose your footing (but not your feet)
stumbleCooldown: 3, // s — punctuation, not a stutter: one gust hold must not stumble you twice
// Shelter (hold C): brace and the wind stops owning you. This is the storm's real answer to "the
// gusts are too strong to cross the yard" — wait one out, then move in the lull.
shelterKnockMult: 2.0, // knockWind × this while braced — a gust that floors you standing won't
shelterShoveMult: 0.25, // and it barely pushes you
// Ladder (decision 12). DESIGN.md: "ladder work at height in wind is genuinely tense" — this is
// where that gets teeth. You cannot brace up there (both hands are on the rungs), so the only
// defence is choosing your moment: climb in a lull, not through a gust.
climbRate: 1.1, // m/s up or down a rung — slow enough that the storm gets a vote
reach: 2.2, // m — how high a 1.72 m person's hands get, standing. Fascia sits at 2.48.
ladderKnockMult: 0.6, // knockWind × this while on the ladder — far easier to be blown off
};
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
/** Shortest-arc angle step from a toward b, at most `maxStep` radians. */
function turnToward(a, b, maxStep) {
let d = (b - a) % (Math.PI * 2);
if (d > Math.PI) d -= Math.PI * 2;
if (d < -Math.PI) d += Math.PI * 2;
return a + clamp(d, -maxStep, maxStep);
}
export class PlayerSim {
/**
* @param {object} [opts]
* @param {object} [opts.start] {x,y,z} spawn, metres
* @param {function} [opts.groundAt] (x,z) -> y. Lane A's world.js provides the real one.
* @param {function} [opts.collide] (x,z,feetY,headY) -> {x,z} pushed clear of world.solids.
* Injected, not imported, for the same reason as groundAt: this file must stay renderer-free.
* player.js#makeSolidCollider builds the real one out of world.solids.
* @param {number} [opts.height] body height, metres — the collider's vertical span
* @param {object} [opts.tune] overrides for TUNE
*/
constructor(opts = {}) {
const s = opts.start || { x: 0, y: 0, z: 0 };
this.pos = { x: s.x, y: s.y || 0, z: s.z };
this.vel = { x: 0, z: 0 }; // intended (input-driven) velocity
this.shove = { x: 0, z: 0 }; // wind-driven velocity, decays through foot friction
this.facing = opts.facing || 0; // yaw; models face +Z at 0 (90sDJsim convention)
this.state = 'idle';
this.stateT = 0;
this.carrying = null; // contract: player.carrying — one item, hands-full rule
this.busyClip = null; // interact.js names the verb for the current hold (Crank, PickUp…)
this.stumbleCool = 0; // s until a gust may stumble you again
this.events = []; // {type:'state'|'drop'|'knockdown', …} drained by the view/HUD
this.exposure = 0; // s spent above knockWind
this.windBase = 0; // EMA baseline (see TUNE.baseTrack)
this.windSpeed = 0; // last sampled local speed, m/s — HUD/audio read this
this.gust = 0; // windSpeed - windBase, clamped ≥0
this.pitch = 0; // 0 upright … 1 flat on the ground
this.knockDir = { x: 0, z: 1 }; // which way the body went down
this.lean = 0; // 0 upright … 1 full wind-lean (view reads this + leanDir)
this.leanDir = { x: 0, z: 1 }; // unit downwind — the way the wind is trying to push you
this.climbY = 0; // m above the ground; >0 means you're up a ladder
this.climbTarget = 0; // where ladder.js asked you to be
this.fellFrom = 0; // m — height of the last fall, for the HUD/aftermath to shame you with
this.groundAt = opts.groundAt || (() => 0);
this.collide = opts.collide || null;
this.bodyHeight = opts.height || 1.72;
this.tune = { ...TUNE, ...(opts.tune || {}) };
}
/** contract: player.busy */
get busy() { return !!STATES[this.state].locked; }
get clip() { return STATES[this.state].clip; }
get speed() { return Math.hypot(this.vel.x, this.vel.z); }
/** Radians of torso tip at a full wind-lean. Surfaced so PlayerView reads it, not TUNE. */
get leanMaxRad() { return this.tune.leanMaxRad; }
/** How high this person's hands get right now. The gate on reaching a fascia bracket. */
get reachY() { return this.pos.y + this.climbY + this.tune.reach; }
/**
* Go up or down a ladder. ladder.js owns WHERE (it knows the rungs); the sim owns the motion, so
* a climb is deterministic and fast-forwards in selftest like everything else.
* @param {number} y target height above ground; 0 climbs back down
*/
climbTo(y, t = 0) {
this.climbTarget = Math.max(0, y);
if (Math.abs(this.climbTarget - this.climbY) > 0.02) {
this.vel.x = this.vel.z = 0;
this.setState('climb', t);
}
return true;
}
setState(s, t = 0) {
if (this.state === s) return false;
if (!STATES[s]) throw new Error(`player: unknown state ${s}`);
this.state = s;
this.stateT = 0;
this.events.push({ type: 'state', state: s, t });
return true;
}
/** Drop whatever's carried (knockdown does this per PLAN3D §5-D.3). */
drop(t = 0) {
if (!this.carrying) return null;
const item = this.carrying;
this.carrying = null;
this.events.push({ type: 'drop', item, t });
return item;
}
/** @returns {boolean} true if the pickup was accepted (hands-full rule). */
pickUp(item, t = 0) {
if (this.carrying || this.busy) return false;
this.carrying = item;
this.events.push({ type: 'pickup', item, t });
return true;
}
/** Light hit — a glancing debris clip. Ignored if already down. */
staggerHit(t = 0) {
if (this.state === 'knocked' || this.state === 'getup') return false;
return this.setState('stagger', t);
}
/**
* Put the player on the ground. debris.js (Lane C) calls this on a solid hit; the sim calls it
* itself on sustained extreme wind.
* @param {number} [dirX] @param {number} [dirZ] which way to fall — defaults to downwind/facing.
*/
knockdown(t = 0, dirX, dirZ) {
if (this.state === 'knocked' || this.state === 'getup') return false;
let x = dirX, z = dirZ;
if (x === undefined || (x === 0 && z === 0)) { x = Math.sin(this.facing); z = Math.cos(this.facing); }
const m = Math.hypot(x, z) || 1;
this.knockDir = { x: x / m, z: z / m };
// Blown off a ladder: you don't stagger, you fall. Everything else about a knockdown is the
// same, so the ladder gets the get-up chain for free — you just arrive on the ground first.
this.fellFrom = this.climbY;
this.climbY = 0;
this.climbTarget = 0;
this.setState('knocked', t);
this.exposure = 0;
this.vel.x = this.vel.z = 0;
this.drop(t);
this.events.push({ type: 'knockdown', t, dir: { ...this.knockDir }, fellFrom: this.fellFrom });
return true;
}
/**
* @param {number} dt fixed step, seconds
* @param {number} t storm time, seconds
* @param {object} input {x,z} camera-relative axes in -1..1, {run}, {camYaw} radians
* @param {object} wind contracts wind ({sample(pos,t)->Vector3}) or a plain {x,z} vector
*/
step(dt, t, input = {}, wind = null) {
const T = this.tune;
this.stateT += dt;
this.stumbleCool = Math.max(0, this.stumbleCool - dt);
// --- local wind, and how much of it is gust ---
let wx = 0, wz = 0;
if (wind) {
const v = typeof wind.sample === 'function' ? wind.sample(this.pos, t) : wind;
if (v) { wx = v.x || 0; wz = v.z || 0; }
}
const ws = Math.hypot(wx, wz);
this.windSpeed = ws;
this.windBase += (ws - this.windBase) * clamp(dt * T.baseTrack, 0, 1);
this.gust = Math.max(0, ws - this.windBase);
// --- shelter: hold to brace. Enters and leaves itself, so releasing the key always frees you
// even mid-gust. Refused while you're down — you can't brace from your back, and refused on
// a ladder: both hands are on the rungs. Up there your only defence is having picked a lull. ---
const up = onLadder(this);
const wantShelter = !!input.shelter;
const canShelter = this.state === 'idle' || this.state === 'walk' || this.state === 'run';
if (wantShelter && canShelter) this.setState('shelter', t);
else if (!wantShelter && this.state === 'shelter') this.setState('idle', t);
const braced = this.state === 'shelter';
// --- sustained extreme wind puts you down (same rule as a sail corner letting go).
// Bracing raises the bar; a ladder LOWERS it. Same exposure clock either way, so the storm
// speaks one language whether you're on your feet or up a rung. ---
const knockAt = T.knockWind
* (braced ? T.shelterKnockMult : 1)
* (up ? T.ladderKnockMult : 1);
if (ws > knockAt) this.exposure += dt;
else this.exposure = Math.max(0, this.exposure - dt * T.knockBleed);
if (this.exposure >= T.knockSustain) this.knockdown(t, wx, wz);
// --- the climb itself: code-driven height, ClimbLadder plays on top (the knockdown precedent) ---
if (this.state === 'climb') {
const dy = this.climbTarget - this.climbY;
const rung = T.climbRate * dt;
if (Math.abs(dy) <= rung) {
this.climbY = this.climbTarget;
this.setState(this.climbY > 0.02 ? 'atTop' : 'idle', t);
} else {
this.climbY += Math.sign(dy) * rung;
}
}
// Invariant: you cannot be standing on the grass while you are 2 m up a ladder. interact.js
// releases a finished hold to 'idle' without knowing where you are; this puts you back in the
// work stance instead of leaving you idling in mid-air.
if (this.climbY > 0.02
&& (this.state === 'idle' || this.state === 'walk' || this.state === 'run')) {
this.setState('atTop', t);
}
// --- a gust below the knockdown bar can still break your stride ---
if (!braced && this.gust > T.stumbleGust && this.stumbleCool <= 0
&& (this.state === 'idle' || this.state === 'walk' || this.state === 'run')) {
this.stumbleCool = T.stumbleCooldown;
this.setState('stumble', t);
this.vel.x = this.vel.z = 0;
}
const st = STATES[this.state];
const aloft = onLadder(this); // re-read: the climb block above may have just landed you
// --- movement ---
const slow = 1 - Math.min(T.slowMax, ws / T.slowRef); // prototype: rain + wind slow you
let wantX = 0, wantZ = 0;
if (!st.locked && !aloft) {
const ix = input.x || 0, iz = input.z || 0;
const mag = Math.hypot(ix, iz);
if (mag > 1e-3) {
// camera-relative: at camYaw 0 three.js looks down -Z, so forward = (-sin, -cos).
const cy = input.camYaw || 0;
const sin = Math.sin(cy), cos = Math.cos(cy);
const nx = ix / mag, nz = iz / mag;
const dx = nx * cos - nz * sin;
const dz = -nx * sin - nz * cos;
const target = (input.run ? T.runSpeed : T.walkSpeed) * Math.min(1, mag) * slow;
wantX = dx * target; wantZ = dz * target;
this.facing = turnToward(this.facing, Math.atan2(dx, dz), T.turnRate * dt);
}
}
// approach the wanted velocity at a fixed accel (both directions — stopping is the same law)
const dvx = wantX - this.vel.x, dvz = wantZ - this.vel.z;
const dvm = Math.hypot(dvx, dvz);
const step = T.accel * dt;
if (dvm <= step || dvm < 1e-6) { this.vel.x = wantX; this.vel.z = wantZ; }
else { this.vel.x += dvx / dvm * step; this.vel.z += dvz / dvm * step; }
// --- gust shove: pressure ∝ speed², gust only, never while you're already on the ground ---
const grounded = this.state === 'knocked' || this.state === 'getup';
if (!grounded && this.gust > T.shoveGustMin && ws > 1e-3) {
const a = T.shoveK * ws * ws * (braced ? T.shelterShoveMult : 1);
this.shove.x += (wx / ws) * a * dt;
this.shove.z += (wz / ws) * a * dt;
}
const bleed = Math.exp(-T.shoveDamp * dt);
this.shove.x *= bleed; this.shove.z *= bleed;
this.pos.x += (this.vel.x + this.shove.x) * dt;
this.pos.z += (this.vel.z + this.shove.z) * dt;
this.pos.y = this.groundAt(this.pos.x, this.pos.z);
// Solids: push back out of anything we ended up inside. Pushout is perpendicular to the surface,
// so walking into a wall at an angle keeps its tangential component and slides along it for free
// — no separate slide pass. Velocity is deliberately NOT zeroed: the wind should still be able to
// hold you against a fence, and the pushout wins over it every frame anyway.
if (this.collide) {
const r = this.collide(this.pos.x, this.pos.z, this.pos.y, this.pos.y + this.bodyHeight);
if (r) { this.pos.x = r.x; this.pos.z = r.z; }
}
// --- body pitch: the sim owns it so a knockdown is deterministic and falls DOWNWIND,
// which a canned clip can't do. player.js just reads sim.pitch + sim.knockDir. ---
const wantPitch = this.state === 'knocked' ? 1
: this.state === 'getup' ? Math.max(0, 1 - this.stateT / (st.secs || 1))
: 0;
const pstep = dt / T.pitchSecs;
this.pitch = clamp(this.pitch + clamp(wantPitch - this.pitch, -pstep, pstep), 0, 1);
// --- wind lean: the sim owns it exactly like pitch, so the view stays a pure reader and the lean
// is deterministic (dt,t). Off windBase, not ws, so a gust punches the stumble/knock event
// while the posture holds. Suppressed where a lean would fight another root pose: on your back
// (pitch owns the body), braced (TakeCover is its own hunch), or up the ladder (you're pinned
// to the rungs, not standing in the wind). It eases to 0 there rather than snapping. ---
const leanOK = this.pitch < 1e-3 && this.state !== 'shelter' && !up;
const leanSpan = Math.max(1e-3, T.leanWindFull - T.leanWindMin);
const wantLean = leanOK
? clamp((this.windBase - T.leanWindMin) / leanSpan, 0, 1)
: 0;
const lstep = dt / T.leanSecs;
this.lean = clamp(this.lean + clamp(wantLean - this.lean, -lstep, lstep), 0, 1);
// downwind is where the wind blows TOWARD; track it only while there's a wind to read, so a lull
// leaves the last bearing rather than snapping leanDir to a default and twisting the body.
if (ws > 0.1) { const inv = 1 / ws; this.leanDir.x = wx * inv; this.leanDir.z = wz * inv; }
// --- locomotion state from actual speed (so shove/slow can't desync the feet).
// `aloft` is what holds you in atTop: it's unlocked (so hold-E works up there), and without
// this guard the speed check would immediately re-state you to idle and drop you off. ---
if (!st.locked && !aloft) {
const sp = this.speed;
this.setState(sp < 0.15 ? 'idle' : sp > T.walkSpeed * 1.35 ? 'run' : 'walk', t);
} else if (st.secs && this.stateT >= st.secs && st.next) {
this.setState(st.next, t);
}
return this.state;
}
}