Floor fan
A configurable direction and switch compose into a reusable pushing tile.
What it demonstrates: Select fields · automatic saving · directional push.
Run this example
All learning examples appear in a compatible developer world. To export a standalone project, an operator with source access runs this from the Scape repository (choose a new destination under an existing directory):
sh
yarn sdk:starter /path/to/my-fan --template fan1
In the exported project:
sh
yarn install
yarn build
yarn dev --origin https://your-scape-host1
2
3
2
3
Follow connection and approval. The exported dev script invokes scape gizmo dev. The kit includes three private archives for the SDK, CLI and MCP server; keep them and the Yarn resolutions together. Third-party dependencies still need installation. See the CLI reference for commands; no public Scape registry install is available yet.
Try changing it
Change its initial direction, then switch it off and on. Return null to stop pushing; the host still owns movement.
Source
This code is included directly from the working example when the site builds.
src/definition.ts
ts
/** A switchable floor fan: state selects a push; Scape handles each player's movement. */
import {
defineObject,
exactKeys,
record,
requirePayload,
ObjectActionError,
type GizmoPush,
} from '@scape/sdk';
const directions = ['right', 'down', 'left', 'up'] as const;
export interface FanState {
on: boolean;
direction: GizmoPush['direction'];
}
export const fan = defineObject<FanState>({
type: 'example.fan',
version: 1,
emoji: '🪭',
label: 'Floor fan',
hint: 'step onto the fan to get pushed; choose its direction in Configure',
initial: () => ({ on: true, direction: 'right' }),
valid: (state): state is FanState =>
record(state) &&
exactKeys(state, ['on', 'direction']) &&
typeof state.on === 'boolean' &&
directions.some((direction) => direction === state.direction),
walkable: true,
// No step callback is needed. Returning null stops pushing without changing walkability.
// Opposing input stays allowed, unlike the conveyor's one-way surface.
push: (state) => (state.on ? { direction: state.direction } : null),
actions: {
toggle: {
permission: 'participant',
run: (state, payload) => {
requirePayload(payload, []);
return { ...state, on: !state.on };
},
},
direction: {
permission: 'editor',
run: (state, payload) => {
requirePayload(payload, ['direction']);
if (!directions.some((direction) => direction === payload.direction))
throw new ObjectActionError(400, 'Choose a direction.');
return { ...state, direction: payload.direction as FanState['direction'] };
},
},
},
view: (state, viewer) => ({
title: 'Floor fan',
description: '',
fields: [
{
id: 'direction',
label: 'Direction',
kind: 'select',
value: state.direction,
options: directions.map((value) => ({
value,
label: value[0].toUpperCase() + value.slice(1),
})),
},
],
controls: [
{
id: 'toggle',
icon: 'toggle',
label: state.on ? 'Turn off' : 'Turn on',
pressed: state.on,
action: { name: 'toggle', payload: {} },
},
{
id: 'direction',
label: 'Set direction',
trigger: 'change',
fields: ['direction'],
disabled: !viewer.canEdit,
action: { name: 'direction', payload: {} },
},
],
}),
});
export default fan;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
src/project.ts
ts
/** Project entry: exports fan to Scape's SDK loader. */
import { defineProject } from '@scape/sdk';
import { fan } from './definition.js';
export default defineProject({ objects: [fan] });1
2
3
4
5
2
3
4
5
