An animation is a value a graph can pick, and playing a running one makes it win again (#97)
Play, Pause and Stop animation take the animation on a port instead of a fixed setting, fed by the new Animation literal or a list item tagged `$animation`. Playing an animation already running keeps its frame but counts it as started last, so turning back to a walk that never stopped shows that walk. The actor demo walks through one Play animation over `ItemAt(walk, facing)` again and stops every animation to stand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5af0a1c83a
commit
4ed2edd0f0
10 files changed
+158
-71
No files matched your search
@@ -1,16 +1,17 @@
|
||||
import { pauseAnimation, playAnimation, stopAnimation } from '../../../model/animation';
|
||||
import { textOf } from '../../../model/Properties';
|
||||
import type { PropertyDef } from '../../../properties/PropertySchema';
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asEntity } from '../../values';
|
||||
import type { NodeDef, PortDef } from '../../NodeRegistry';
|
||||
import { asEntity, asString } from '../../values';
|
||||
import { exec, obj } from '../shared';
|
||||
|
||||
const animationParam = (): PropertyDef => ({
|
||||
/**
|
||||
* The `animation` input of the three animation nodes; unconnected it shows the animation picker, the
|
||||
* way `soundPort` shows the sound picker. An `Animation` literal or a list item is what feeds it.
|
||||
*/
|
||||
const animationPort = (): PortDef => ({
|
||||
key: 'animation',
|
||||
label: 'Animation',
|
||||
kind: 'asset',
|
||||
default: '',
|
||||
assetTypes: ['animation'],
|
||||
label: 'animation',
|
||||
type: 'string',
|
||||
field: { key: 'animation', label: 'Animation', kind: 'animation', default: '' },
|
||||
});
|
||||
|
||||
export const PlayAnimation: NodeDef = {
|
||||
@@ -19,13 +20,14 @@ export const PlayAnimation: NodeDef = {
|
||||
category: 'action',
|
||||
description:
|
||||
'Plays an animation on an object: its rows reach the object and what is placed inside it, so every ' +
|
||||
'instance plays on its own. A paused one runs on from where it stands; one already running is left alone.',
|
||||
inputs: [exec(), obj('object')],
|
||||
'instance plays on its own. A paused one runs on from where it stands; one already running keeps its ' +
|
||||
'frame but wins again over the others on the object.',
|
||||
inputs: [exec(), obj('object'), animationPort()],
|
||||
outputs: [exec()],
|
||||
params: [animationParam()],
|
||||
run: (_ctx, inputs, params) => {
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
const animation = textOf(params.animation ?? '');
|
||||
const animation = asString(inputs.animation);
|
||||
if (target && animation) playAnimation(target, animation);
|
||||
return { next: 'exec' };
|
||||
},
|
||||
@@ -37,12 +39,12 @@ export const StopAnimation: NodeDef = {
|
||||
category: 'action',
|
||||
description:
|
||||
'Ends an animation on an object where it stands – the values it set stay. Leave Animation at None to end every one.',
|
||||
inputs: [exec(), obj('object')],
|
||||
inputs: [exec(), obj('object'), animationPort()],
|
||||
outputs: [exec()],
|
||||
params: [animationParam()],
|
||||
run: (_ctx, inputs, params) => {
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
if (target) stopAnimation(target, textOf(params.animation ?? ''));
|
||||
if (target) stopAnimation(target, asString(inputs.animation));
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -53,12 +55,12 @@ export const PauseAnimation: NodeDef = {
|
||||
category: 'action',
|
||||
description:
|
||||
'Holds an animation on an object at its frame until Play animation lets it run on. Leave Animation at None to hold every one.',
|
||||
inputs: [exec(), obj('object')],
|
||||
inputs: [exec(), obj('object'), animationPort()],
|
||||
outputs: [exec()],
|
||||
params: [animationParam()],
|
||||
run: (_ctx, inputs, params) => {
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const target = asEntity(inputs.object);
|
||||
if (target) pauseAnimation(target, textOf(params.animation ?? ''));
|
||||
if (target) pauseAnimation(target, asString(inputs.animation));
|
||||
return { next: 'exec' };
|
||||
},
|
||||
};
|
||||
@@ -45,6 +45,11 @@ export const SceneLiteral = literal(
|
||||
'scene',
|
||||
'A constant scene, for a "Load scene" to work on.',
|
||||
);
|
||||
export const AnimationLiteral = literal(
|
||||
'Animation',
|
||||
'animation',
|
||||
'A constant animation, for a "Play animation", "Pause animation" or "Stop animation" to work on.',
|
||||
);
|
||||
export const ListLiteral = literal(
|
||||
'List',
|
||||
'list',
|
||||
|
||||
@@ -33,6 +33,7 @@ import { Join } from './data/Join';
|
||||
import { KeyIsDown } from './data/KeyIsDown';
|
||||
import { Length } from './data/Length';
|
||||
import {
|
||||
AnimationLiteral,
|
||||
BooleanLiteral,
|
||||
ListLiteral,
|
||||
NumberLiteral,
|
||||
@@ -156,6 +157,7 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
StateMachineLiteral,
|
||||
SoundLiteral,
|
||||
SceneLiteral,
|
||||
AnimationLiteral,
|
||||
Add,
|
||||
Subtract,
|
||||
Multiply,
|
||||
|
||||
@@ -219,14 +219,16 @@ interface PlayerHolder {
|
||||
let startCount = 0;
|
||||
|
||||
/**
|
||||
* Starts the animation on the object, or lets a paused one run on. One already running is left
|
||||
* alone, so a graph may ask for it every frame without holding it at its first frame.
|
||||
* Starts the animation on the object, or lets a paused one run on. One already running keeps its
|
||||
* frame, so a graph may ask for it every frame without holding it at its first frame – but it counts
|
||||
* as started last again: turning back to a walk that still runs has to show that walk.
|
||||
*/
|
||||
export function playAnimation(holder: PlayerHolder, animationId: string): void {
|
||||
const players = (holder.animations ??= []);
|
||||
const known = players.find((p) => p.animationId === animationId);
|
||||
if (known) {
|
||||
known.playing = true;
|
||||
known.order = ++startCount;
|
||||
return;
|
||||
}
|
||||
players.push({ animationId, time: 0, playing: true, order: ++startCount });
|
||||
|
||||
@@ -134,6 +134,20 @@ export const KINDS = [
|
||||
field: true,
|
||||
port: true,
|
||||
},
|
||||
/** An animation asset's id – a text, so it is tagged in a list like a texture. */
|
||||
{
|
||||
kind: 'animation',
|
||||
label: 'Animation',
|
||||
mark: '▶',
|
||||
travels: 'string',
|
||||
empty: '',
|
||||
coerce: text,
|
||||
tagged: true,
|
||||
reference: true,
|
||||
variable: true,
|
||||
field: true,
|
||||
port: true,
|
||||
},
|
||||
{ kind: 'object', label: 'Object', mark: '◇', travels: 'object', port: true },
|
||||
{ kind: 'list', label: 'List', mark: '≡', travels: 'list', variable: true, port: true },
|
||||
{ kind: 'struct', label: 'Struct', mark: '⊞', travels: 'struct', variable: true, port: true },
|
||||
|
||||
@@ -247,6 +247,11 @@ const REFERENCES: Record<ReferenceKind, Reference> = {
|
||||
label: assetRefLabel,
|
||||
choices: (project) => assetRefChoices(project, ['scene']).map((a) => ({ id: a.id, label: a.name })),
|
||||
},
|
||||
animation: {
|
||||
none: NO_ASSET_LABEL,
|
||||
label: assetRefLabel,
|
||||
choices: (project) => assetRefChoices(project, ['animation']).map((a) => ({ id: a.id, label: a.name })),
|
||||
},
|
||||
};
|
||||
|
||||
/** An id in the words of its reference kind; nothing for a kind that names nothing. */
|
||||
@@ -374,6 +379,7 @@ export function createField(
|
||||
case 'statemachine':
|
||||
case 'sound':
|
||||
case 'scene':
|
||||
case 'animation':
|
||||
return referenceField(ctx, def, REFERENCES[def.kind], value, handlers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +357,21 @@ describe('the AnimationSystem', () => {
|
||||
expect(sprite.x).toBeCloseTo(20);
|
||||
});
|
||||
|
||||
it('lets a running animation win again when it is played again, without winding it back', () => {
|
||||
const { f, walk, sprite, frame } = slide();
|
||||
const hold = addAnimation(f, f.spriteId, {}, 'Hold');
|
||||
addRow(f, hold, 'x', [[0, -1]]);
|
||||
playAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
playAnimation(sprite, hold.id);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(-1);
|
||||
// Wound back it would read 0, still beaten -1: 20 is the walk at its third tick.
|
||||
playAnimation(sprite, walk.id);
|
||||
frame(0.1);
|
||||
expect(sprite.x).toBeCloseTo(20);
|
||||
});
|
||||
|
||||
it('stands still while the object is switched off', () => {
|
||||
const { f, walk, box } = slide();
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, box, 'enabled', false));
|
||||
|
||||
@@ -477,6 +477,10 @@ const CASES: Record<string, Case[]> = {
|
||||
{ name: 'outputs the id it was set to', params: { value: 'sc-1' }, outputs: { value: 'sc-1' } },
|
||||
{ name: 'unset is no scene', outputs: { value: '' } },
|
||||
],
|
||||
Animation: [
|
||||
{ name: 'outputs the id it was set to', params: { value: 'anim-1' }, outputs: { value: 'anim-1' } },
|
||||
{ name: 'unset is no animation', outputs: { value: '' } },
|
||||
],
|
||||
Add: [
|
||||
{ name: 'adds', inputs: { a: 2, b: 3 }, outputs: { result: 5 } },
|
||||
{ name: 'numeric text is coerced', inputs: { a: '2', b: 3 }, outputs: { result: 5 } },
|
||||
@@ -859,14 +863,12 @@ const CASES: Record<string, Case[]> = {
|
||||
PlayAnimation: [
|
||||
{
|
||||
name: 'no object is a no-op that still continues the flow',
|
||||
inputs: { object: null },
|
||||
params: { animation: 'anim-1' },
|
||||
inputs: { object: null, animation: 'anim-1' },
|
||||
next: 'exec',
|
||||
},
|
||||
{
|
||||
name: 'starts a player on the object, and a second play leaves it running',
|
||||
inputs: { object: dancer },
|
||||
params: { animation: 'anim-1' },
|
||||
inputs: { object: dancer, animation: 'anim-1' },
|
||||
next: 'exec',
|
||||
after: () => {
|
||||
expect(dancer.animations).toMatchObject([{ animationId: 'anim-1', time: 0, playing: true }]);
|
||||
@@ -876,8 +878,7 @@ const CASES: Record<string, Case[]> = {
|
||||
PauseAnimation: [
|
||||
{
|
||||
name: 'holds the player where it stands',
|
||||
inputs: { object: dancer },
|
||||
params: { animation: 'anim-1' },
|
||||
inputs: { object: dancer, animation: 'anim-1' },
|
||||
next: 'exec',
|
||||
after: () => expect(dancer.animations?.map((p) => p.playing)).toEqual([false]),
|
||||
},
|
||||
@@ -885,8 +886,7 @@ const CASES: Record<string, Case[]> = {
|
||||
StopAnimation: [
|
||||
{
|
||||
name: 'with no animation named ends every player on the object',
|
||||
inputs: { object: dancer },
|
||||
params: { animation: '' },
|
||||
inputs: { object: dancer, animation: '' },
|
||||
next: 'exec',
|
||||
after: () => expect(dancer.animations).toEqual([]),
|
||||
},
|
||||
|
||||
+31
-19
@@ -7,13 +7,13 @@
|
||||
* (down, left, right, up). This demo uses the character in the top left corner, so its frames are
|
||||
* the columns 1-3 of the rows 1-4.
|
||||
*
|
||||
* Walking is one animation per direction, playing those three frames as `pingpong`
|
||||
* (left-stand-right-stand …) – the original walk cycle. Standing is not an animation at all but the
|
||||
* middle frame of the row.
|
||||
* Walking is one animation per direction over the sprite: a `texture` row keying those three frames,
|
||||
* played `pingpong` (left-stand-right-stand …) – the original walk cycle. Standing is not an
|
||||
* animation at all but the middle frame of the row.
|
||||
*
|
||||
* The graph picks a picture the way the editor does: two list variables hold the four walk
|
||||
* animations and the four standing frames, each item chosen with the texture picker, and `facing`
|
||||
* is the row index that indexes both. Nothing is built out of an id spelled by hand.
|
||||
* The graph picks the way the editor does: two list variables hold the four walk animations and the
|
||||
* four standing frames, each item chosen with its picker, and `facing` is the row index that indexes
|
||||
* both. Nothing is built out of an id spelled by hand.
|
||||
*
|
||||
* Usage: node tools/seed-actor.mjs [projectId] [baseUrl]
|
||||
*/
|
||||
@@ -68,11 +68,19 @@ const walkAnimations = DIRECTIONS.map((dir, row) => ({
|
||||
type: 'animation',
|
||||
name: `Walk ${dir}`,
|
||||
props: { fps: 8, playback: 'pingpong' },
|
||||
frames: [frameId(row, 0), frameId(row, 1), frameId(row, 2)],
|
||||
root: 'actor-sprite',
|
||||
tracks: [
|
||||
{
|
||||
id: `${WALK(dir)}-texture`,
|
||||
target: '',
|
||||
property: 'texture',
|
||||
keys: [0, 1, 2].map((column) => ({ frame: column, value: frameId(row, column) })),
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
/** The two lists the graph indexes: one walk animation and one standing frame per direction. */
|
||||
const WALK_PICTURES = DIRECTIONS.map((dir) => tagged('texture', WALK(dir)));
|
||||
const WALK_ANIMATIONS = DIRECTIONS.map((dir) => tagged('animation', WALK(dir)));
|
||||
const IDLE_PICTURES = DIRECTIONS.map((_, row) => tagged('texture', idleFrame(row)));
|
||||
|
||||
/**
|
||||
@@ -129,9 +137,10 @@ const nodes = [
|
||||
face('c-up', 'up', 1920, 940),
|
||||
face('c-down', 'down', 1920, 1080),
|
||||
|
||||
// What the sprite shows. Both picture lists are indexed by the row the character faces, so there
|
||||
// is one node per list and no branch per direction.
|
||||
// Setting the same animation again lets it keep running, so writing it every frame is free.
|
||||
// What the sprite shows. Both lists are indexed by the row the character faces, so there is one
|
||||
// node per list and no branch per direction. Playing a running walk again keeps its frame but makes
|
||||
// it the one that wins, so turning back to a walk still running shows that walk. Standing stops
|
||||
// every animation and wears the standing frame.
|
||||
node('br-move', 'Branch', 1460, 1280),
|
||||
node('v-pics', 'Variables', 1400, 1480, {}, [
|
||||
port('walk', 'list'),
|
||||
@@ -140,8 +149,9 @@ const nodes = [
|
||||
]),
|
||||
node('pick-walk', 'ItemAt', 1680, 1430),
|
||||
node('pick-idle', 'ItemAt', 1680, 1640),
|
||||
node('wear-walk', 'Properties', 1920, 1280, propertiesOf('sprite'), [written('texture', 'texture')]),
|
||||
node('wear-idle', 'Properties', 1920, 1510, propertiesOf('sprite'), [written('texture', 'texture')]),
|
||||
node('play-walk', 'PlayAnimation', 1920, 1280),
|
||||
node('stop-walk', 'StopAnimation', 1920, 1510),
|
||||
node('wear-idle', 'Properties', 2160, 1510, propertiesOf('sprite'), [written('texture', 'texture')]),
|
||||
];
|
||||
|
||||
const edges = [
|
||||
@@ -198,12 +208,14 @@ const edges = [
|
||||
edge('e48', 'v-pics', 'facing', 'pick-walk', 'index'),
|
||||
edge('e49', 'v-pics', 'idle', 'pick-idle', 'list'),
|
||||
edge('e50', 'v-pics', 'facing', 'pick-idle', 'index'),
|
||||
edge('e51', 'br-move', 'then', 'wear-walk', 'exec'),
|
||||
edge('e52', 'owner', 'object', 'wear-walk', 'object'),
|
||||
edge('e53', 'pick-walk', 'item', 'wear-walk', 'texture'),
|
||||
edge('e54', 'br-move', 'else', 'wear-idle', 'exec'),
|
||||
edge('e51', 'br-move', 'then', 'play-walk', 'exec'),
|
||||
edge('e52', 'owner', 'object', 'play-walk', 'object'),
|
||||
edge('e53', 'pick-walk', 'item', 'play-walk', 'animation'),
|
||||
edge('e54', 'br-move', 'else', 'stop-walk', 'exec'),
|
||||
edge('e55', 'owner', 'object', 'wear-idle', 'object'),
|
||||
edge('e56', 'pick-idle', 'item', 'wear-idle', 'texture'),
|
||||
edge('e57', 'owner', 'object', 'stop-walk', 'object'),
|
||||
edge('e58', 'stop-walk', 'exec', 'wear-idle', 'exec'),
|
||||
];
|
||||
|
||||
const sheet = await uploadTexture(baseUrl, SHEET_FILE);
|
||||
@@ -251,11 +263,11 @@ const project = {
|
||||
id: 'player',
|
||||
type: 'behavior',
|
||||
name: 'Player',
|
||||
props: { speed: SPEED, facing: 0, walk: WALK_PICTURES, idle: IDLE_PICTURES },
|
||||
props: { speed: SPEED, facing: 0, walk: WALK_ANIMATIONS, idle: IDLE_PICTURES },
|
||||
variables: [
|
||||
variable('speed', 'number', SPEED),
|
||||
variable('facing', 'number', 0),
|
||||
variable('walk', 'list', WALK_PICTURES),
|
||||
variable('walk', 'list', WALK_ANIMATIONS),
|
||||
variable('idle', 'list', IDLE_PICTURES),
|
||||
],
|
||||
graph: { nodes, edges },
|
||||
|
||||
+49
-20
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Plays the walk demo (tools/seed-actor.mjs) in a headless browser: standing, walking in four
|
||||
* directions, the diagonal, the turn when a key is let go, and the edge of the camera. It is the
|
||||
* proof that animations, `Axis input` and a sliced sheet work together.
|
||||
* proof that `Play animation`, `Axis input` and a sliced sheet work together.
|
||||
*
|
||||
* Usage: node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||
* Screenshots land in /tmp/gm-actor-*.png.
|
||||
@@ -13,8 +13,8 @@ import { PROJECTS, cliArgs } from './lib/projects.mjs';
|
||||
const { projectId, baseUrl } = cliArgs('smoke-actor.mjs', PROJECTS.actor);
|
||||
|
||||
/** What the seeder writes. A mismatch means the loader dropped a node type or an edge. */
|
||||
const NODES = 44;
|
||||
const EDGES = 56;
|
||||
const NODES = 45;
|
||||
const EDGES = 58;
|
||||
|
||||
/**
|
||||
* The standing frame of each direction: the middle column of its row in the sheet. The graph reaches
|
||||
@@ -28,6 +28,15 @@ const CELL = 48;
|
||||
const EDGE_X = 296;
|
||||
const EDGE_Y = 156;
|
||||
|
||||
/** The walk that decides the picture: of the animations running on him, the one started last. */
|
||||
const walkOf = (e) =>
|
||||
(e.animations ?? [])
|
||||
.filter((p) => p.playing)
|
||||
.reduce((best, p) => (best && best.order > p.order ? best : p), null)?.animationId ?? '';
|
||||
|
||||
/** The row of the sheet a frame id lies in – what a picture of a direction looks like. */
|
||||
const rowOfFrame = (texture) => Number(String(texture).split('-')[1]);
|
||||
|
||||
await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, async (t) => {
|
||||
const { check, sleep, json, shot, entity, play, stop, navigate, exceptions, holdKey, keyDown, keyUp } = t;
|
||||
|
||||
@@ -58,14 +67,22 @@ await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, asyn
|
||||
JSON.stringify(sheet.last),
|
||||
);
|
||||
|
||||
// 3. Four walk animations, three frames each, ping-pong.
|
||||
// 3. Four walk animations over the sprite, each one texture row of three keys, ping-pong.
|
||||
const anims = await json(
|
||||
`__gm.state.project.assets.filter((a) => a.type === 'animation').map((a) => ({ id: a.id, n: a.frames.length, fps: a.props.fps, pb: a.props.playback, mid: a.frames[1] }))`,
|
||||
`__gm.state.project.assets.filter((a) => a.type === 'animation').map((a) => ({ id: a.id, root: a.root, rows: a.tracks.map((t) => t.target + ':' + t.property), n: a.tracks[0]?.keys.length, fps: a.props.fps, pb: a.props.playback, mid: a.tracks[0]?.keys[1]?.value }))`,
|
||||
);
|
||||
check(
|
||||
'four walk animations of three frames play ping-pong',
|
||||
anims.length === 4 && anims.every((a) => a.n === 3 && a.pb === 'pingpong' && a.fps === 8),
|
||||
JSON.stringify(anims.map((a) => `${a.id}:${a.n}`)),
|
||||
'four walk animations key three frames on the sprite and play ping-pong',
|
||||
anims.length === 4 &&
|
||||
anims.every(
|
||||
(a) =>
|
||||
a.root === 'actor-sprite' &&
|
||||
a.rows.join() === ':texture' &&
|
||||
a.n === 3 &&
|
||||
a.pb === 'pingpong' &&
|
||||
a.fps === 8,
|
||||
),
|
||||
JSON.stringify(anims.map((a) => `${a.id}:${a.root}:${a.rows}:${a.n}`)),
|
||||
);
|
||||
check(
|
||||
'each walk animation has the standing frame in the middle',
|
||||
@@ -74,15 +91,17 @@ await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, asyn
|
||||
);
|
||||
|
||||
// 3b. How the graph reaches a picture: two list variables of tagged texture items, indexed by the
|
||||
// row the character faces. Each item is what the texture picker writes, not a pasted id.
|
||||
// row the character faces. Each item is what its picker writes, not a pasted id.
|
||||
const pictures = await json(
|
||||
`(() => { const b = __gm.state.project.assets.find((a) => a.id === 'player'); return { walk: b.props.walk, idle: b.props.idle, facing: b.props.facing }; })()`,
|
||||
);
|
||||
check(
|
||||
'the two picture lists hold four tagged textures each, indexed by facing',
|
||||
'the walk list holds four tagged animations, the idle list four tagged textures, indexed by facing',
|
||||
pictures.walk.length === 4 &&
|
||||
pictures.idle.length === 4 &&
|
||||
pictures.walk.every((item, row) => item.$texture === `walk-${['down', 'left', 'right', 'up'][row]}`) &&
|
||||
pictures.walk.every(
|
||||
(item, row) => item.$animation === `walk-${['down', 'left', 'right', 'up'][row]}`,
|
||||
) &&
|
||||
pictures.idle.every((item, row) => item.$texture === `fr-${row}-1`) &&
|
||||
pictures.facing === 0,
|
||||
JSON.stringify(pictures),
|
||||
@@ -113,15 +132,23 @@ await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, asyn
|
||||
const still = await entity('oplayer');
|
||||
check(
|
||||
'holding D walks right and plays the right walk animation',
|
||||
walking.x > 30 && walking.props.texture === 'walk-right',
|
||||
JSON.stringify({ x: Math.round(walking.x), texture: walking.props.texture }),
|
||||
walking.x > 30 && walkOf(walking) === 'walk-right' && rowOfFrame(walking.props.texture) === 2,
|
||||
JSON.stringify({ x: Math.round(walking.x), walk: walkOf(walking), texture: walking.props.texture }),
|
||||
);
|
||||
// Two samples after the release: he must not move between them. Comparing against the walking
|
||||
// sample measured how long the screenshot took (22 to 40 px on the CI runner), not the stop.
|
||||
check(
|
||||
'letting go stops him on the standing frame of the way he faces',
|
||||
stopped.props.texture === IDLE.right && Math.abs(still.x - stopped.x) < 1 && stopped.x > walking.x - 1,
|
||||
JSON.stringify({ x: Math.round(stopped.x), later: Math.round(still.x), texture: stopped.props.texture }),
|
||||
stopped.props.texture === IDLE.right &&
|
||||
walkOf(stopped) === '' &&
|
||||
Math.abs(still.x - stopped.x) < 1 &&
|
||||
stopped.x > walking.x - 1,
|
||||
JSON.stringify({
|
||||
x: Math.round(stopped.x),
|
||||
later: Math.round(still.x),
|
||||
walk: walkOf(stopped),
|
||||
texture: stopped.props.texture,
|
||||
}),
|
||||
);
|
||||
|
||||
// 6. Walking up is −y.
|
||||
@@ -144,8 +171,10 @@ await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, asyn
|
||||
await shot('04-diagonal');
|
||||
check(
|
||||
'pressing W while D is held turns him up, the key pressed last',
|
||||
beforeW.props.texture === 'walk-right' && diagonal.props.texture === 'walk-up',
|
||||
JSON.stringify({ beforeW: beforeW.props.texture, diagonal: diagonal.props.texture }),
|
||||
walkOf(beforeW) === 'walk-right' &&
|
||||
walkOf(diagonal) === 'walk-up' &&
|
||||
rowOfFrame(diagonal.props.texture) === 3,
|
||||
JSON.stringify({ beforeW: walkOf(beforeW), diagonal: walkOf(diagonal), texture: diagonal.props.texture }),
|
||||
);
|
||||
check(
|
||||
'the diagonal moves both axes at once',
|
||||
@@ -160,9 +189,9 @@ await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, asyn
|
||||
await keyUp('d');
|
||||
await sleep(200);
|
||||
check(
|
||||
'releasing W while D is held turns him back to the right',
|
||||
afterW.props.texture === 'walk-right',
|
||||
JSON.stringify({ texture: afterW.props.texture }),
|
||||
'releasing W while D is held turns him back to the right, onto the walk still running',
|
||||
walkOf(afterW) === 'walk-right' && rowOfFrame(afterW.props.texture) === 2,
|
||||
JSON.stringify({ walk: walkOf(afterW), texture: afterW.props.texture }),
|
||||
);
|
||||
|
||||
// 9. `Axis input` hands back a normalized vector, so a diagonal is not a faster way to travel.
|
||||
|
||||
Reference in new issue
Block a user