@v12-engine/create 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/base/_gitignore +5 -0
- package/base/package.json +3 -3
- package/base/src/main.ts +36 -10
- package/base/src/navigation.worker.ts +7 -0
- package/base/vite.config.ts +24 -2
- package/cli.mjs +21 -6
- package/index.mjs +27 -7
- package/layers/desktop/electron/app.mjs +237 -0
- package/layers/desktop/electron/config.mjs +16 -0
- package/layers/desktop/electron/dev.mjs +63 -0
- package/layers/desktop/electron/files.mjs +86 -0
- package/layers/desktop/electron/icons/icon.icns +0 -0
- package/layers/desktop/electron/icons/icon.png +0 -0
- package/layers/desktop/electron/main.mjs +12 -0
- package/layers/desktop/electron/package.mjs +75 -0
- package/layers/desktop/electron/preload.cjs +41 -0
- package/layers/desktop/electron/presentation.m +87 -0
- package/layers/desktop/electron/serve.mjs +76 -0
- package/layers/desktop/electron/tools.mjs +63 -0
- package/layers/desktop/electron/updates.mjs +71 -0
- package/layers/desktop/package.json +13 -0
- package/layers/desktop/src/desktop.ts +175 -0
- package/layers/desktop/src/shell.ts +279 -0
- package/layers/desktop/tests/main.test.ts +189 -0
- package/layers/desktop/tests/shell.test.ts +152 -0
- package/layers/rooms/server.ts +13 -10
- package/layers/server/server.ts +4 -5
- package/layers/source/vite.config.ts +21 -1
- package/package.json +2 -1
- package/starters/campaign/README.md +9 -2
- package/starters/campaign/layers/rooms/server.ts +3 -3
- package/starters/campaign/layers/server/server.ts +3 -5
- package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
- package/starters/campaign/src/campaign/game.ts +26 -5
- package/starters/campaign/src/campaign/index.ts +104 -68
- package/starters/campaign/src/campaign/kills.ts +1 -1
- package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
- package/starters/campaign/src/campaign/missions/siege.ts +6 -6
- package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
- package/starters/campaign/src/campaign/scenes.ts +8 -7
- package/starters/campaign/src/campaign/units.ts +59 -20
- package/starters/campaign/src/campaign/view/attacks.ts +19 -0
- package/starters/campaign/src/campaign/view/fire.ts +2 -2
- package/starters/campaign/src/campaign/view/index.ts +54 -43
- package/starters/campaign/src/campaign/waves.ts +18 -17
- package/starters/campaign/src/game.ts +8 -1
- package/starters/campaign/src/main.ts +38 -19
- package/starters/campaign/src/maps/fort.names.ts +2 -2
- package/starters/campaign/src/maps/fort.vgmap.json +102 -2
- package/starters/campaign/src/maps/homestead.names.ts +2 -2
- package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
- package/starters/campaign/src/maps/pass.names.ts +2 -2
- package/starters/campaign/src/maps/pass.vgmap.json +69 -1
- package/starters/campaign/src/waves-panel.ts +2 -1
- package/starters/empty/README.md +18 -10
- package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
- package/starters/empty/src/game.ts +11 -75
- package/starters/empty/src/generate.worker.ts +5 -5
- package/starters/empty/src/rules.ts +83 -25
- package/starters/empty/src/view.ts +82 -0
- package/starters/examples/README.md +83 -0
- package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
- package/starters/examples/package.json +5 -0
- package/starters/examples/src/examples/abilities/areas.ts +165 -0
- package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
- package/starters/examples/src/examples/abilities/buffs.ts +287 -0
- package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
- package/starters/examples/src/examples/abilities/changing.ts +228 -0
- package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
- package/starters/examples/src/examples/abilities/control.ts +158 -0
- package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
- package/starters/examples/src/examples/abilities/damage.ts +156 -0
- package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
- package/starters/examples/src/examples/abilities/family.ts +29 -0
- package/starters/examples/src/examples/abilities/index.ts +32 -0
- package/starters/examples/src/examples/abilities/kit.ts +179 -0
- package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
- package/starters/examples/src/examples/abilities/looks.ts +47 -0
- package/starters/examples/src/examples/abilities/missiles.ts +203 -0
- package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
- package/starters/examples/src/examples/abilities/moves.ts +206 -0
- package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
- package/starters/examples/src/examples/abilities/recasts.ts +181 -0
- package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
- package/starters/examples/src/examples/abilities/resources.ts +166 -0
- package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
- package/starters/examples/src/examples/abilities/summons.ts +296 -0
- package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
- package/starters/examples/src/examples/abilities/triggers.ts +204 -0
- package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
- package/starters/examples/src/examples/abilities/vision.ts +137 -0
- package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
- package/starters/examples/src/examples/animation/index.ts +235 -0
- package/starters/examples/src/examples/animation/looks.ts +444 -0
- package/starters/examples/src/examples/catalog.ts +52 -0
- package/starters/examples/src/examples/cinematics/index.ts +217 -0
- package/starters/examples/src/examples/cinematics/looks.ts +476 -0
- package/starters/examples/src/examples/cinematics/tour.json +43 -0
- package/starters/examples/src/examples/cinematics/village.ts +88 -0
- package/starters/examples/src/examples/common/looks.ts +65 -0
- package/starters/examples/src/examples/common/units.ts +70 -0
- package/starters/examples/src/examples/construction/index.ts +329 -0
- package/starters/examples/src/examples/construction/looks.ts +275 -0
- package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
- package/starters/examples/src/examples/custom-animation/index.ts +190 -0
- package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
- package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
- package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
- package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
- package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
- package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
- package/starters/examples/src/examples/custom-fx/index.ts +25 -0
- package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
- package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
- package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
- package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
- package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
- package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
- package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
- package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
- package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
- package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
- package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
- package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
- package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
- package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
- package/starters/examples/src/examples/define.ts +86 -0
- package/starters/examples/src/examples/destruction/index.ts +570 -0
- package/starters/examples/src/examples/destruction/looks.ts +447 -0
- package/starters/examples/src/examples/dialogs/index.ts +383 -0
- package/starters/examples/src/examples/dialogs/looks.ts +546 -0
- package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
- package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
- package/starters/examples/src/examples/fx/blood.look.ts +62 -0
- package/starters/examples/src/examples/fx/blood.ts +59 -0
- package/starters/examples/src/examples/fx/feel.look.ts +86 -0
- package/starters/examples/src/examples/fx/feel.ts +57 -0
- package/starters/examples/src/examples/fx/fight.ts +28 -0
- package/starters/examples/src/examples/fx/floats.look.ts +108 -0
- package/starters/examples/src/examples/fx/floats.ts +24 -0
- package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
- package/starters/examples/src/examples/fx/gallery.ts +23 -0
- package/starters/examples/src/examples/fx/index.ts +30 -0
- package/starters/examples/src/examples/fx/links.look.ts +83 -0
- package/starters/examples/src/examples/fx/links.ts +49 -0
- package/starters/examples/src/examples/fx/looks.ts +55 -0
- package/starters/examples/src/examples/fx/map.look.ts +93 -0
- package/starters/examples/src/examples/fx/map.ts +33 -0
- package/starters/examples/src/examples/fx/marks.look.ts +100 -0
- package/starters/examples/src/examples/fx/marks.ts +36 -0
- package/starters/examples/src/examples/fx/shots.look.ts +92 -0
- package/starters/examples/src/examples/fx/shots.ts +71 -0
- package/starters/examples/src/examples/fx/spells.look.ts +87 -0
- package/starters/examples/src/examples/fx/spells.ts +122 -0
- package/starters/examples/src/examples/fx/stage.ts +31 -0
- package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
- package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
- package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
- package/starters/examples/src/examples/gui/index.ts +143 -0
- package/starters/examples/src/examples/gui/keys.ts +91 -0
- package/starters/examples/src/examples/gui/layout.ts +42 -0
- package/starters/examples/src/examples/gui/looks.ts +270 -0
- package/starters/examples/src/examples/gui/minimap.ts +127 -0
- package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
- package/starters/examples/src/examples/gui/plates.ts +87 -0
- package/starters/examples/src/examples/gui/radial.ts +104 -0
- package/starters/examples/src/examples/gui/solidPart.ts +107 -0
- package/starters/examples/src/examples/looks.ts +45 -0
- package/starters/examples/src/examples/mechanics/battle.ts +51 -0
- package/starters/examples/src/examples/mechanics/damage.ts +80 -0
- package/starters/examples/src/examples/mechanics/flying.ts +93 -0
- package/starters/examples/src/examples/mechanics/formations.ts +58 -0
- package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
- package/starters/examples/src/examples/mechanics/index.ts +44 -0
- package/starters/examples/src/examples/mechanics/looks.ts +289 -0
- package/starters/examples/src/examples/mechanics/maze.ts +119 -0
- package/starters/examples/src/examples/mechanics/orders.ts +68 -0
- package/starters/examples/src/examples/mechanics/selection.ts +30 -0
- package/starters/examples/src/examples/moba/index.ts +377 -0
- package/starters/examples/src/examples/moba/looks.ts +349 -0
- package/starters/examples/src/examples/moba/maps.ts +69 -0
- package/starters/examples/src/examples/moba/spells.ts +34 -0
- package/starters/examples/src/examples/moba/units.ts +196 -0
- package/starters/examples/src/examples/nature/camera.ts +15 -0
- package/starters/examples/src/examples/nature/grass.ts +64 -0
- package/starters/examples/src/examples/nature/index.ts +327 -0
- package/starters/examples/src/examples/nature/land.ts +173 -0
- package/starters/examples/src/examples/nature/looks.ts +577 -0
- package/starters/examples/src/examples/nature/maps.ts +370 -0
- package/starters/examples/src/examples/nature/skies.ts +324 -0
- package/starters/examples/src/examples/nature/weather.ts +80 -0
- package/starters/examples/src/examples/performance/index.ts +263 -0
- package/starters/examples/src/examples/performance/looks.ts +372 -0
- package/starters/examples/src/examples/performance/meter.ts +158 -0
- package/starters/examples/src/examples/post/grades.ts +86 -0
- package/starters/examples/src/examples/post/index.ts +202 -0
- package/starters/examples/src/examples/post/looks.ts +884 -0
- package/starters/examples/src/examples/post/passes.ts +213 -0
- package/starters/examples/src/examples/rts/ai.ts +49 -0
- package/starters/examples/src/examples/rts/airlift.ts +43 -0
- package/starters/examples/src/examples/rts/base.ts +129 -0
- package/starters/examples/src/examples/rts/build.ts +65 -0
- package/starters/examples/src/examples/rts/camps.ts +36 -0
- package/starters/examples/src/examples/rts/carry.ts +103 -0
- package/starters/examples/src/examples/rts/creeps.ts +61 -0
- package/starters/examples/src/examples/rts/data.ts +406 -0
- package/starters/examples/src/examples/rts/ferry.ts +41 -0
- package/starters/examples/src/examples/rts/garrison.ts +50 -0
- package/starters/examples/src/examples/rts/harvest.ts +29 -0
- package/starters/examples/src/examples/rts/hero.ts +67 -0
- package/starters/examples/src/examples/rts/index.ts +77 -0
- package/starters/examples/src/examples/rts/looks.ts +721 -0
- package/starters/examples/src/examples/rts/shop.ts +46 -0
- package/starters/examples/src/examples/rts/tech.ts +39 -0
- package/starters/examples/src/examples/rts/train.ts +48 -0
- package/starters/examples/src/examples/rts/waterside.ts +185 -0
- package/starters/examples/src/examples/showcase/index.ts +714 -0
- package/starters/examples/src/examples/showcase/looks.ts +238 -0
- package/starters/examples/src/game.ts +17 -0
- package/starters/examples/src/main.ts +82 -0
- package/starters/examples/src/rules.ts +31 -0
- package/starters/examples/src/shell/look.ts +79 -0
- package/starters/examples/src/shell/registry.ts +7 -0
- package/starters/examples/src/shell/scenes.ts +178 -0
- package/starters/examples/src/shell/shell.ts +290 -0
- package/starters/examples/src/shell/style.ts +202 -0
- package/starters/examples/src/view.ts +50 -0
- package/starters/melee/README.md +12 -3
- package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
- package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
- package/starters/melee/src/game.ts +32 -2
- package/starters/melee/src/maps/bay.names.ts +2 -2
- package/starters/melee/src/maps/bay.vgmap.json +198 -1
- package/starters/melee/src/maps.ts +11 -9
- package/starters/melee/src/melee/abilities.ts +1 -194
- package/starters/melee/src/melee/blight.ts +7 -7
- package/starters/melee/src/melee/cellLayers.json +3 -3
- package/starters/melee/src/melee/data.json +1 -18
- package/starters/melee/src/melee/game.ts +6 -4
- package/starters/melee/src/melee/heroes.ts +1025 -0
- package/starters/melee/src/melee/index.ts +64 -61
- package/starters/melee/src/melee/items.ts +15 -362
- package/starters/melee/src/melee/languages/he.json +14 -2
- package/starters/melee/src/melee/pings.ts +5 -6
- package/starters/melee/src/melee/saves.ts +1 -1
- package/starters/melee/src/melee/undead.ts +15 -230
- package/starters/melee/src/melee/units.ts +37 -171
- package/starters/melee/src/melee/view/attacks.ts +19 -0
- package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
- package/starters/melee/src/melee/view/index.ts +56 -52
- package/starters/melee/src/melee/view/looks.ts +86 -63
- package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
- package/starters/melee/src/melee/view/walls.ts +1 -1
- package/starters.mjs +6 -0
- package/v12.mjs +14 -15
- package/starters/campaign/src/rules.ts +0 -20
- package/starters/empty/src/maps.ts +0 -44
- package/starters/empty/src/tiles.ts +0 -12
- package/starters/melee/src/melee/heroSpells.ts +0 -38
- package/starters/melee/src/rules.ts +0 -24
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The showcases on the client's side: the looks of their own units (the boss and its dead, the towers, the raiders and
|
|
3
|
+
* the keep), the effects their rules play, and cards that read how each scene stands from what the rules tell every
|
|
4
|
+
* screen (a component replicated on an entity: `EntityState.components`)
|
|
5
|
+
*/
|
|
6
|
+
import type { EntityView, GameClientApi, GameView, UnitLook } from '@v12-engine/client'
|
|
7
|
+
import { presetFx } from '@v12-engine/client'
|
|
8
|
+
import { button, type CategoryLooks, readout, select, toggle } from '../../shell/look'
|
|
9
|
+
import { character, hold, person, TWO_HANDED } from '../common/looks'
|
|
10
|
+
import { ENEMY, PLAYER } from '../common/units'
|
|
11
|
+
import {
|
|
12
|
+
bossCharge,
|
|
13
|
+
bossReset,
|
|
14
|
+
buildTower,
|
|
15
|
+
callWave,
|
|
16
|
+
defenseReset,
|
|
17
|
+
generalsReset,
|
|
18
|
+
halt,
|
|
19
|
+
marchAs,
|
|
20
|
+
rewind,
|
|
21
|
+
rewindFight,
|
|
22
|
+
rewindReset,
|
|
23
|
+
SHOW,
|
|
24
|
+
TOWER_COST
|
|
25
|
+
} from './index'
|
|
26
|
+
|
|
27
|
+
/** The effects the showcases' rules play (`world.fx`), taken into the game's look */
|
|
28
|
+
const EFFECTS = ['dust', 'shockwave', 'explosion', 'ground-crack', 'impact-ring'] as const
|
|
29
|
+
|
|
30
|
+
const looks: Record<string, UnitLook> = {
|
|
31
|
+
[SHOW.showcaseBoss]: {
|
|
32
|
+
height: 4.4,
|
|
33
|
+
model: character('skeleton_warrior', 1.9, { hold: [hold('sword_2handed_color')] }),
|
|
34
|
+
attacks: [TWO_HANDED],
|
|
35
|
+
// red with rage in its last phase: the buff it carries
|
|
36
|
+
tintFor: (entity: EntityView) => (entity.state.buffs?.includes('showcase.enraged') ? [1, 0.45, 0.4] : undefined),
|
|
37
|
+
death: { clip: 'Skeletons_Death', fx: 'explosion', corpse: { lie: 6, sink: 3 } },
|
|
38
|
+
shapes: person(4.4)
|
|
39
|
+
},
|
|
40
|
+
[SHOW.showcaseRisen]: {
|
|
41
|
+
height: 1.6,
|
|
42
|
+
model: character('skeleton_minion', 0.65, { animations: { idle: 'Skeletons_Idle', walk: 'Skeletons_Walking' } }),
|
|
43
|
+
death: { shatter: 10 },
|
|
44
|
+
shapes: person(1.6)
|
|
45
|
+
},
|
|
46
|
+
[SHOW.showcaseTower]: {
|
|
47
|
+
height: 3.2,
|
|
48
|
+
model: { src: 'rts/small_watch_tower.glb', scale: 1.15 },
|
|
49
|
+
shot: { color: [0.9, 0.8, 0.55], size: 0.2 },
|
|
50
|
+
shapes: [{ shape: 'cylinder', size: [1.2, 3, 1.2], offset: [0, 1.5, 0], color: [0.6, 0.5, 0.4] }]
|
|
51
|
+
},
|
|
52
|
+
[SHOW.showcaseRaider]: {
|
|
53
|
+
height: 1.6,
|
|
54
|
+
model: character('skeleton_rogue', 0.62, { hold: [hold('dagger')], animations: { walk: 'Skeletons_Walking' } }),
|
|
55
|
+
death: { shatter: 8 },
|
|
56
|
+
shapes: person(1.6)
|
|
57
|
+
},
|
|
58
|
+
[SHOW.showcaseBrute]: {
|
|
59
|
+
height: 2.2,
|
|
60
|
+
model: character('skeleton_warrior', 0.95, { hold: [hold('axe_1handed'), hold('shield_spikes', 'l')] }),
|
|
61
|
+
death: { shatter: 14, fx: 'dust' },
|
|
62
|
+
shapes: person(2.2)
|
|
63
|
+
},
|
|
64
|
+
[SHOW.showcaseKeep]: {
|
|
65
|
+
height: 4,
|
|
66
|
+
model: { src: 'rts/castle.glb', scale: 0.75 },
|
|
67
|
+
shapes: [{ shape: 'box', size: [4, 3, 4], offset: [0, 1.5, 0], color: [0.6, 0.6, 0.65] }]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The first unit of a type the client knows, alive */
|
|
72
|
+
const first = (client: GameClientApi, type: string) => [...client.state.entities.values()].find((each) => each.def.id === type && each.health > 0)
|
|
73
|
+
/** How many of a side are alive */
|
|
74
|
+
const count = (client: GameClientApi, owner: number) =>
|
|
75
|
+
[...client.state.entities.values()].filter((each) => each.owner === owner && each.health > 0).length
|
|
76
|
+
/** A component the rules tell every screen, on a unit of a type: what the snapshot carries of it */
|
|
77
|
+
function told<T>(client: GameClientApi, type: string, component: string): T | undefined {
|
|
78
|
+
const entity = client.session.snapshot().entities.find((each) => each.type === type && each.health > 0)
|
|
79
|
+
return entity?.components?.[component] as T | undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Puts the camera at a point, `distance` back and as steep as `pitch`, letting it go that far and a half again */
|
|
83
|
+
const stage = (client: GameClientApi, x: number, z: number, distance: number, pitch: number, yaw = 0) => {
|
|
84
|
+
client.camera.setLimits({ max: Math.max(distance * 1.5, 60) })
|
|
85
|
+
client.camera.setPose({ x, z, distance, yaw, pitch })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const category: CategoryLooks = {
|
|
89
|
+
view: (view: GameView<any>) => ({
|
|
90
|
+
...view,
|
|
91
|
+
units: { ...view.units, looks: { ...view.units.looks, ...looks } },
|
|
92
|
+
fx: { ...view.fx, defs: { ...presetFx([...EFFECTS]), ...view.fx?.defs } }
|
|
93
|
+
}),
|
|
94
|
+
looks: {
|
|
95
|
+
'showcase-boss': {
|
|
96
|
+
controls: [
|
|
97
|
+
button('Charge!', (client) => client.session.send(bossCharge.make({})), 'Sends the party at the Bone King, fighting whatever is in the way'),
|
|
98
|
+
button(
|
|
99
|
+
'Start the fight again',
|
|
100
|
+
(client) => client.session.send(bossReset.make({})),
|
|
101
|
+
'The party and the Bone King back where they started, whole, his dead gone'
|
|
102
|
+
),
|
|
103
|
+
readout(
|
|
104
|
+
'The Bone King',
|
|
105
|
+
(client) => {
|
|
106
|
+
const boss = first(client, SHOW.showcaseBoss)
|
|
107
|
+
if (!boss) return 'fallen!'
|
|
108
|
+
const fight = told<{ phase: number }>(client, SHOW.showcaseBoss, 'showcase.bossFight')
|
|
109
|
+
const phase = ['', 'phase 1', 'phase 2: the dead rise', 'phase 3: enraged'][fight?.phase ?? 1]
|
|
110
|
+
return `${Math.round((boss.health / boss.maxHealth) * 100)}% · ${phase}`
|
|
111
|
+
},
|
|
112
|
+
'His health left, and his phase: at two thirds the dead rise, at a third he is enraged'
|
|
113
|
+
),
|
|
114
|
+
readout(
|
|
115
|
+
'Party · his dead',
|
|
116
|
+
(client) => {
|
|
117
|
+
const dead = [...client.state.entities.values()].filter((each) => each.def.id === SHOW.showcaseRisen && each.health > 0).length
|
|
118
|
+
return `${count(client, PLAYER)} · ${dead}`
|
|
119
|
+
},
|
|
120
|
+
'Your party left alive, and the risen dead fighting for him'
|
|
121
|
+
)
|
|
122
|
+
],
|
|
123
|
+
ready: (client) => stage(client, 24, 23, 28, 0.8)
|
|
124
|
+
},
|
|
125
|
+
'showcase-defense': {
|
|
126
|
+
controls: [
|
|
127
|
+
button(
|
|
128
|
+
`Build a tower (${TOWER_COST} gold)`,
|
|
129
|
+
(client) => client.session.send(buildTower.make({})),
|
|
130
|
+
'A tower on the next free spot by the road, if you have the gold: it shoots raiders as they pass'
|
|
131
|
+
),
|
|
132
|
+
button(
|
|
133
|
+
'Call the next wave now',
|
|
134
|
+
(client) => client.session.send(callWave.make({})),
|
|
135
|
+
'The next wave comes at once rather than in a few seconds, and you get fifteen gold for not waiting'
|
|
136
|
+
),
|
|
137
|
+
button(
|
|
138
|
+
'Start again',
|
|
139
|
+
(client) => client.session.send(defenseReset.make({})),
|
|
140
|
+
'Takes everything away and holds the road again from the first wave'
|
|
141
|
+
),
|
|
142
|
+
readout(
|
|
143
|
+
'Wave · lives · gold',
|
|
144
|
+
(client) => {
|
|
145
|
+
const defense = told<{ wave: number; lives: number; gold: number }>(client, SHOW.showcaseKeep, 'showcase.defense')
|
|
146
|
+
return defense ? `${defense.wave} · ${defense.lives} · ${defense.gold}` : '…'
|
|
147
|
+
},
|
|
148
|
+
'The wave it is, the lives the keep has left (a raider at the keep takes one, a brute three), and your gold'
|
|
149
|
+
),
|
|
150
|
+
readout(
|
|
151
|
+
'Raiders on the road · towers',
|
|
152
|
+
(client) => {
|
|
153
|
+
const all = [...client.state.entities.values()].filter((each) => each.health > 0)
|
|
154
|
+
const raiders = all.filter((each) => each.def.id === SHOW.showcaseRaider || each.def.id === SHOW.showcaseBrute).length
|
|
155
|
+
return `${raiders} · ${all.filter((each) => each.def.id === SHOW.showcaseTower).length}`
|
|
156
|
+
},
|
|
157
|
+
'Raiders alive on the road now, and your towers'
|
|
158
|
+
)
|
|
159
|
+
],
|
|
160
|
+
ready: (client) => stage(client, 24, 20, 46, 0.95)
|
|
161
|
+
},
|
|
162
|
+
'showcase-generals': {
|
|
163
|
+
controls: [
|
|
164
|
+
button(
|
|
165
|
+
'Lay them out again',
|
|
166
|
+
(client) => client.session.send(generalsReset.make({})),
|
|
167
|
+
'Both armies whole and back in place: the two computer generals fight it out again'
|
|
168
|
+
),
|
|
169
|
+
readout('Blue · red', (client) => `${count(client, PLAYER)} · ${count(client, ENEMY)}`, 'Units each side has left alive')
|
|
170
|
+
],
|
|
171
|
+
ready: (client) => stage(client, 32, 28, 36, 0.8)
|
|
172
|
+
},
|
|
173
|
+
'showcase-rewind': {
|
|
174
|
+
controls: [
|
|
175
|
+
button(
|
|
176
|
+
'Fight',
|
|
177
|
+
(client) => client.session.send(rewindFight.make({})),
|
|
178
|
+
'Sends both sides at each other: let them fight a while before turning back'
|
|
179
|
+
),
|
|
180
|
+
button(
|
|
181
|
+
'Back 3 seconds',
|
|
182
|
+
(client) => client.session.send(rewind.make({ seconds: 3 })),
|
|
183
|
+
'Every unit back where it stood and as hurt as it was 3 seconds ago, the fallen up again: the fight goes on from there'
|
|
184
|
+
),
|
|
185
|
+
button('Back 10 seconds', (client) => client.session.send(rewind.make({ seconds: 10 })), 'The same, 10 seconds back'),
|
|
186
|
+
button(
|
|
187
|
+
'From the start',
|
|
188
|
+
(client) => client.session.send(rewindReset.make({})),
|
|
189
|
+
'The battle laid out as it began, the record of it started over'
|
|
190
|
+
),
|
|
191
|
+
readout(
|
|
192
|
+
'Blue · red',
|
|
193
|
+
(client) => `${count(client, PLAYER)} · ${count(client, ENEMY)}`,
|
|
194
|
+
'Units each side has left alive: turn back and the fallen count again'
|
|
195
|
+
)
|
|
196
|
+
],
|
|
197
|
+
ready: (client) => stage(client, 24, 21, 24, 0.75)
|
|
198
|
+
},
|
|
199
|
+
'showcase-march': {
|
|
200
|
+
controls: [
|
|
201
|
+
select(
|
|
202
|
+
'Formation',
|
|
203
|
+
['rows', 'square', 'circle'],
|
|
204
|
+
'rows',
|
|
205
|
+
(client, shape) => client.session.send(marchAs.make({ shape: shape as 'rows' })),
|
|
206
|
+
'The shape each of the three companies takes behind the knight: they change it as they march'
|
|
207
|
+
),
|
|
208
|
+
toggle(
|
|
209
|
+
'Halt',
|
|
210
|
+
false,
|
|
211
|
+
(client, halted) => client.session.send(halt.make({ halted })),
|
|
212
|
+
'On, the knight and the companies stop where they are; off, they march on'
|
|
213
|
+
),
|
|
214
|
+
button(
|
|
215
|
+
'Follow the knight',
|
|
216
|
+
(client) => {
|
|
217
|
+
const knight = [...client.state.entities.values()].find((each) => each.def.id === 'knight' && each.health > 0)
|
|
218
|
+
if (knight) client.follow(knight.id)
|
|
219
|
+
},
|
|
220
|
+
'The camera follows the knight at the head of the march'
|
|
221
|
+
),
|
|
222
|
+
button(
|
|
223
|
+
'Look over the field',
|
|
224
|
+
(client) => {
|
|
225
|
+
client.follow(null)
|
|
226
|
+
stage(client, 32, 28, 60, 0.95)
|
|
227
|
+
},
|
|
228
|
+
'The camera stops following and looks down on the whole road'
|
|
229
|
+
),
|
|
230
|
+
readout('Marching', (client) => `${count(client, PLAYER)} in three companies`, 'Everyone on the march, the knight too')
|
|
231
|
+
],
|
|
232
|
+
ready: (client) => {
|
|
233
|
+
client.camera.setLimits({ max: 90 })
|
|
234
|
+
stage(client, 32, 28, 60, 0.95)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The game whole: its rules (`src/rules.ts`) and its look (`src/view.ts`), fetched as the page first plays it. What the
|
|
3
|
+
* page, the server and the tests take; a server or a test never fetches the look
|
|
4
|
+
*/
|
|
5
|
+
import { game } from './rules'
|
|
6
|
+
|
|
7
|
+
// @if !desktop
|
|
8
|
+
game.view(() => import('./view'))
|
|
9
|
+
// @end
|
|
10
|
+
// @if desktop
|
|
11
|
+
// its look with what the desktop app adds to it: a Windowed setting, Quit on the pause menu
|
|
12
|
+
game.view(() => Promise.all([import('./view'), import('./desktop')]).then(([{ view }, { desktopView }]) => desktopView(view)))
|
|
13
|
+
// @end
|
|
14
|
+
|
|
15
|
+
export { game } from './rules'
|
|
16
|
+
export { examples, exampleNamed } from './examples/catalog'
|
|
17
|
+
export { ENEMY, PLAYER, UNIT } from './examples/common/units'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page: the examples' list first (`src/shell/scenes.ts`), each example a scene of an app of scenes (`startApp`,
|
|
3
|
+
* `@v12-engine/client/app`), one shown at a time. `appScenes` makes the usual ones beside them: the start screen,
|
|
4
|
+
* the game on its field, and the settings.
|
|
5
|
+
// @if editor
|
|
6
|
+
* The map editor too, set up in `src/editor.ts`.
|
|
7
|
+
// @end
|
|
8
|
+
// @if server
|
|
9
|
+
* A seat at the game's server too (`npm run server`).
|
|
10
|
+
// @end
|
|
11
|
+
* The address opens straight into a scene by its id: `/?fx-fireball`, an example; `/?home`, the start screen; the
|
|
12
|
+
* examples' list otherwise. The debug panel (🐞 on the top bar, Alt+Shift+D) and its cheats are in the game while
|
|
13
|
+
* developing (`npm run dev`), not in a built page
|
|
14
|
+
*/
|
|
15
|
+
// @if !desktop
|
|
16
|
+
import { appScenes, sceneAsked, sceneRef, startApp } from '@v12-engine/client/app'
|
|
17
|
+
// @end
|
|
18
|
+
// @if desktop
|
|
19
|
+
import { appScenes, exitButton, sceneAsked, sceneRef, startApp } from '@v12-engine/client/app'
|
|
20
|
+
// @end
|
|
21
|
+
import '@v12-engine/client/styles.css'
|
|
22
|
+
// the theme's typefaces come with the client: the page's own font stays what it was
|
|
23
|
+
import '@v12-engine/client/fonts.css'
|
|
24
|
+
// @if editor
|
|
25
|
+
import { editor } from './editor'
|
|
26
|
+
// @end
|
|
27
|
+
import { game } from './game'
|
|
28
|
+
import { EXAMPLES, exampleScenes, examplesIndex } from './shell/scenes'
|
|
29
|
+
// @if desktop
|
|
30
|
+
// what the desktop app adds: its shell's stores, the game saved as the window closes, an error log, updates, Exit
|
|
31
|
+
import { desktop } from './desktop'
|
|
32
|
+
// @end
|
|
33
|
+
|
|
34
|
+
// @if desktop
|
|
35
|
+
const shell = desktop()
|
|
36
|
+
|
|
37
|
+
// @end
|
|
38
|
+
const scenes = appScenes({
|
|
39
|
+
// the game (`src/game.ts`): its look (and the client that draws it) is fetched as a scene first plays it, so that the
|
|
40
|
+
// list shows at once
|
|
41
|
+
game,
|
|
42
|
+
// @if editor
|
|
43
|
+
editor,
|
|
44
|
+
// @end
|
|
45
|
+
// @if single
|
|
46
|
+
server: true,
|
|
47
|
+
// @end
|
|
48
|
+
// @if rooms
|
|
49
|
+
server: { rooms: true },
|
|
50
|
+
// @end
|
|
51
|
+
// the examples' list first on the start screen, and every example among the scenes
|
|
52
|
+
home: {
|
|
53
|
+
buttons: [{ label: 'Examples', scene: sceneRef(EXAMPLES) }],
|
|
54
|
+
// @if desktop
|
|
55
|
+
// Exit, last on the start screen: in the shell alone, as a page cannot close its tab
|
|
56
|
+
after: [exitButton(shell)]
|
|
57
|
+
// @end
|
|
58
|
+
},
|
|
59
|
+
extra: [examplesIndex, ...exampleScenes()],
|
|
60
|
+
menu: { description: 'What V12 can do, a small scene each.' },
|
|
61
|
+
// the Credits on the start screen (`creditsScene`): who made the game, by parts, scrolling
|
|
62
|
+
credits: {
|
|
63
|
+
sections: [
|
|
64
|
+
{ title: 'Made by', names: ['You'] },
|
|
65
|
+
{ title: 'Made with', names: ['V12'] }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/** The ground's textures made in a worker of the page's (`src/ground.worker.ts`), made at the first texture: the page goes on while they are made */
|
|
71
|
+
const groundWorker = () => new Worker(new URL('./ground.worker.ts', import.meta.url), { type: 'module' })
|
|
72
|
+
/** The navigations of its worlds built in another (`src/navigation.worker.ts`), made at the first build */
|
|
73
|
+
const navigationWorker = () => new Worker(new URL('./navigation.worker.ts', import.meta.url), { type: 'module' })
|
|
74
|
+
|
|
75
|
+
// @if !desktop
|
|
76
|
+
const app = startApp({ target: '#game', game, scenes, start: sceneAsked(scenes, EXAMPLES), groundWorker, navigationWorker })
|
|
77
|
+
// @end
|
|
78
|
+
// @if desktop
|
|
79
|
+
const app = startApp({ target: '#game', game, scenes, start: sceneAsked(scenes, EXAMPLES), groundWorker, navigationWorker, ...shell.options })
|
|
80
|
+
shell.started(app)
|
|
81
|
+
// @end
|
|
82
|
+
Object.assign(window, { app })
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules: one game (`createGame`) that every example plays, each with a map and a start of its own. What the
|
|
3
|
+
* examples share is here (the ground's tiles, the shared units: `src/examples/common/units.ts`); what each category
|
|
4
|
+
* adds (its units, abilities, commands) is its folder's `index.ts`, registered below. The engine alone
|
|
5
|
+
* (`@v12-engine/engine`), so the page, the server and the tests play the same game
|
|
6
|
+
*/
|
|
7
|
+
import { createGame, hotGame } from '@v12-engine/engine'
|
|
8
|
+
import { categories } from './examples/catalog'
|
|
9
|
+
import { sharedUnits } from './examples/common/units'
|
|
10
|
+
import { exampleField, tiles } from './examples/define'
|
|
11
|
+
|
|
12
|
+
export const game = createGame({
|
|
13
|
+
// the maps of this game say they are its own by this id
|
|
14
|
+
id: 'examples',
|
|
15
|
+
name: 'Examples',
|
|
16
|
+
// a view that names what the rules do not have (a look for no unit type) stops the page, saying what was meant
|
|
17
|
+
checkView: 'throw',
|
|
18
|
+
tiles,
|
|
19
|
+
// nobody wins or loses: an example plays until it is left
|
|
20
|
+
victory: null
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
sharedUnits(game)
|
|
24
|
+
// each category's units, abilities and commands
|
|
25
|
+
for (const category of Object.values(categories)) category.rules?.(game)
|
|
26
|
+
|
|
27
|
+
// the game's one map of its own, what a server plays and the editor opens: each example says its own (`ExampleDef.map`)
|
|
28
|
+
game.maps({ field: exampleField() })
|
|
29
|
+
|
|
30
|
+
// saved while it runs (npm run dev): the game and the editor open take the new version, with no new page
|
|
31
|
+
if (import.meta.hot) import.meta.hot.accept((module) => hotGame(import.meta.hot, game, module?.game))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an example is on the client's side (`ExampleLook`): how it changes the game's look, the controls listed beside
|
|
3
|
+
* it, and what it does once its game is drawn. A category's `looks.ts` gives them by the example's id. Types alone of
|
|
4
|
+
* the client here: this file is part of the list's page, which carries none of the client's code
|
|
5
|
+
*/
|
|
6
|
+
import type { GameClientApi, GameView } from '@v12-engine/client'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A control beside an example: a button, an on and off, a slider or a choice. Each runs with the example's client, and
|
|
10
|
+
* says what it does (`hint`): shown under the controls while the pointer is over it, and as its tooltip
|
|
11
|
+
*/
|
|
12
|
+
export type ExampleControl =
|
|
13
|
+
| { kind: 'button'; label: string; hint: string; run: (client: GameClientApi) => void }
|
|
14
|
+
| { kind: 'toggle'; label: string; hint: string; value: boolean; set: (client: GameClientApi, on: boolean) => void }
|
|
15
|
+
| {
|
|
16
|
+
kind: 'slider'
|
|
17
|
+
label: string
|
|
18
|
+
hint: string
|
|
19
|
+
min: number
|
|
20
|
+
max: number
|
|
21
|
+
step?: number
|
|
22
|
+
value: number
|
|
23
|
+
set: (client: GameClientApi, value: number) => void
|
|
24
|
+
}
|
|
25
|
+
| { kind: 'select'; label: string; hint: string; options: readonly string[]; value: string; set: (client: GameClientApi, value: string) => void }
|
|
26
|
+
| { kind: 'text'; label: string; hint?: string; text: (client: GameClientApi) => string }
|
|
27
|
+
|
|
28
|
+
/** An example's own on the client's side */
|
|
29
|
+
export interface ExampleLook {
|
|
30
|
+
/** The game's look for this example: units, abilities, the HUD, renderers of its own over the game's */
|
|
31
|
+
view?: (view: GameView<any>) => GameView<any>
|
|
32
|
+
/** Its controls, listed under its summary */
|
|
33
|
+
controls?: readonly ExampleControl[]
|
|
34
|
+
/**
|
|
35
|
+
* Called once its game is drawn, with the client: the camera put where it should look, a unit selected, a light
|
|
36
|
+
* added. Returns what undoes it as the example is left, when anything needs to be
|
|
37
|
+
*/
|
|
38
|
+
ready?: (client: GameClientApi) => void | (() => void)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What a category's `looks.ts` gives: its examples' looks by id, and what its examples all need of the game's look */
|
|
42
|
+
export interface CategoryLooks {
|
|
43
|
+
looks?: Readonly<Record<string, ExampleLook>>
|
|
44
|
+
/** Laid over the whole game's look once (its units', abilities' and effects' looks), before any example's own */
|
|
45
|
+
view?: (view: GameView<any>) => GameView<any>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A button, and what pressing it does */
|
|
49
|
+
export const button = (label: string, run: (client: GameClientApi) => void, hint: string): ExampleControl => ({ kind: 'button', label, run, hint })
|
|
50
|
+
/** An on and off, `value` at first, and what it turns on */
|
|
51
|
+
export const toggle = (label: string, value: boolean, set: (client: GameClientApi, on: boolean) => void, hint: string): ExampleControl => ({
|
|
52
|
+
kind: 'toggle',
|
|
53
|
+
label,
|
|
54
|
+
value,
|
|
55
|
+
set,
|
|
56
|
+
hint
|
|
57
|
+
})
|
|
58
|
+
/** A slider from `min` to `max`, `value` at first */
|
|
59
|
+
export const slider = (
|
|
60
|
+
label: string,
|
|
61
|
+
range: { min: number; max: number; step?: number; value: number },
|
|
62
|
+
set: (client: GameClientApi, value: number) => void,
|
|
63
|
+
hint: string
|
|
64
|
+
): ExampleControl => ({ kind: 'slider', label, ...range, set, hint })
|
|
65
|
+
/** A choice of `options`, `value` at first */
|
|
66
|
+
export const select = (
|
|
67
|
+
label: string,
|
|
68
|
+
options: readonly string[],
|
|
69
|
+
value: string,
|
|
70
|
+
set: (client: GameClientApi, value: string) => void,
|
|
71
|
+
hint: string
|
|
72
|
+
): ExampleControl => ({ kind: 'select', label, options, value, set, hint })
|
|
73
|
+
/** A line of text kept up to date (twice a second), and what it counts when that is not plain from its label */
|
|
74
|
+
export const readout = (label: string, text: (client: GameClientApi) => string, hint?: string): ExampleControl => ({
|
|
75
|
+
kind: 'text',
|
|
76
|
+
label,
|
|
77
|
+
text,
|
|
78
|
+
...(hint && { hint })
|
|
79
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the examples' looks are kept once the game's look is fetched (`src/view.ts` fills it): what an example's scene
|
|
3
|
+
* reads as it opens. Nothing of the client's code here, so the list's page can name it
|
|
4
|
+
*/
|
|
5
|
+
import type { ExampleLook } from './look'
|
|
6
|
+
|
|
7
|
+
export const registry: { looks: Readonly<Record<string, ExampleLook>> } = { looks: {} }
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The examples as scenes of the app: the list's own page (`examples`, a card of each example by category) and a game
|
|
3
|
+
* scene for each example, its id its scene's (`/?fx-fireball` opens it straight). Each is drawn in the frame
|
|
4
|
+
* (`./shell.ts`): the list down the left, the example on the right with its card. The client's code is fetched as an
|
|
5
|
+
* example first opens, not with the list
|
|
6
|
+
*/
|
|
7
|
+
import { gameScene, type Scene, type SceneOpenContext, sceneRef } from '@v12-engine/client/app'
|
|
8
|
+
import type { GameClientApi, GameView, LoadProgress } from '@v12-engine/client'
|
|
9
|
+
import { errorFields } from '@v12-engine/engine'
|
|
10
|
+
import { game } from '../game'
|
|
11
|
+
import { examples } from '../examples/catalog'
|
|
12
|
+
import { CATEGORIES, exampleField, type ExampleDef } from '../examples/define'
|
|
13
|
+
import type { ExampleLook } from './look'
|
|
14
|
+
import { registry } from './registry'
|
|
15
|
+
import { mountShell, type Shell } from './shell'
|
|
16
|
+
|
|
17
|
+
/** The list's page's id: where an example's leave button goes */
|
|
18
|
+
export const EXAMPLES = 'examples'
|
|
19
|
+
|
|
20
|
+
/** The categories whose examples show what a player has (gold, lumber, supply) on the HUD's top */
|
|
21
|
+
const RESOURCES_SHOWN: readonly string[] = ['rts', 'moba']
|
|
22
|
+
|
|
23
|
+
/** Opens an example by its id, or the list's page for none */
|
|
24
|
+
const opener = (context: SceneOpenContext<any>) => (id: string) => context.goTo(sceneRef(id || EXAMPLES))
|
|
25
|
+
|
|
26
|
+
/** Fetches an example ahead of its opening: its code and the files it starts with (models, textures), once */
|
|
27
|
+
const warmer = (context: SceneOpenContext<any>) => (id: string) => void context.warm?.(id)
|
|
28
|
+
|
|
29
|
+
/** What the frame is told by a scene: how to open another, warm it, and go to the start screen */
|
|
30
|
+
const shellOptions = (context: SceneOpenContext<any>) => ({
|
|
31
|
+
open: opener(context),
|
|
32
|
+
warm: warmer(context),
|
|
33
|
+
home: () => context.goTo(sceneRef('home'))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** The list's own page: every example as a card, by category */
|
|
37
|
+
export const examplesIndex: Scene = {
|
|
38
|
+
id: EXAMPLES,
|
|
39
|
+
name: 'Examples',
|
|
40
|
+
names: [sceneRef('home')],
|
|
41
|
+
open(element, context) {
|
|
42
|
+
const shell = mountShell(element, shellOptions(context))
|
|
43
|
+
const page = document.createElement('div')
|
|
44
|
+
page.className = 'vx-index'
|
|
45
|
+
page.innerHTML = `<h1>Examples</h1><p>What the engine can do, a small scene each. Pick one on the left or below: it plays at
|
|
46
|
+
once, with its controls on a card over it. Each is a few dozen lines in <code>src/examples/</code>, its rules in its
|
|
47
|
+
category's <code>index.ts</code> and its look and controls in its <code>looks.ts</code>: read one, copy it, change it.</p>`
|
|
48
|
+
for (const category of CATEGORIES) {
|
|
49
|
+
const own = examples.filter((each) => each.category === category.id)
|
|
50
|
+
if (!own.length) continue
|
|
51
|
+
const title = document.createElement('h2')
|
|
52
|
+
title.textContent = category.title
|
|
53
|
+
const about = document.createElement('p')
|
|
54
|
+
about.textContent = category.summary
|
|
55
|
+
const grid = document.createElement('div')
|
|
56
|
+
grid.className = 'vx-grid'
|
|
57
|
+
for (const def of own) {
|
|
58
|
+
const tile = document.createElement('a')
|
|
59
|
+
tile.className = 'vx-tile'
|
|
60
|
+
tile.href = `?${def.id}`
|
|
61
|
+
tile.innerHTML = '<strong></strong><span></span>'
|
|
62
|
+
tile.querySelector('strong')!.textContent = def.title
|
|
63
|
+
tile.querySelector('span')!.textContent = def.summary
|
|
64
|
+
tile.onclick = (event) => {
|
|
65
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey) return
|
|
66
|
+
event.preventDefault()
|
|
67
|
+
context.goTo(sceneRef(def.id))
|
|
68
|
+
}
|
|
69
|
+
tile.onpointerenter = () => void context.warm?.(def.id)
|
|
70
|
+
grid.append(tile)
|
|
71
|
+
}
|
|
72
|
+
page.append(title, about, grid)
|
|
73
|
+
}
|
|
74
|
+
shell.content.append(page)
|
|
75
|
+
return () => shell.dispose()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** An example's scene: its game in the frame, its look laid over the game's, its controls on its card once it is drawn */
|
|
80
|
+
export function exampleScene(def: ExampleDef): Scene {
|
|
81
|
+
// the frame of the scene open now, which the view's frame phase tells once the game is drawn
|
|
82
|
+
const open: { shell?: Shell } = {}
|
|
83
|
+
const scene = gameScene({
|
|
84
|
+
id: def.id,
|
|
85
|
+
name: def.title,
|
|
86
|
+
game,
|
|
87
|
+
map: def.map ?? (() => exampleField()),
|
|
88
|
+
// the example's own start, or none: the game's (a scout) is for the field alone
|
|
89
|
+
onStart: def.start ?? (() => {}),
|
|
90
|
+
...(def.environment && { environment: def.environment }),
|
|
91
|
+
reveal: def.reveal ?? true,
|
|
92
|
+
exit: { label: 'Examples', scene: sceneRef(EXAMPLES) },
|
|
93
|
+
view: (view) => withExample(view, def, registry.looks[def.id], (client, look) => open.shell?.controls(look, client)),
|
|
94
|
+
// shown at once, the list beside it, with how far its files are: not the scene before it held until it is ready
|
|
95
|
+
loading: { mount: (element, progress) => loadingScreen(element, def.title, progress) }
|
|
96
|
+
})
|
|
97
|
+
return {
|
|
98
|
+
...scene,
|
|
99
|
+
names: [sceneRef(EXAMPLES), sceneRef('home')],
|
|
100
|
+
open(element, context) {
|
|
101
|
+
const shell = mountShell(element, { current: def, ...shellOptions(context) })
|
|
102
|
+
open.shell = shell
|
|
103
|
+
const drawn = document.createElement('div')
|
|
104
|
+
drawn.className = 'vx-game'
|
|
105
|
+
shell.content.prepend(drawn)
|
|
106
|
+
const stop = scene.open(drawn, context)
|
|
107
|
+
return () => {
|
|
108
|
+
stop()
|
|
109
|
+
shell.dispose()
|
|
110
|
+
if (open.shell === shell) open.shell = undefined
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The game's look with an example's over it, and a phase of the frame that, at the first frame, hands the example's
|
|
118
|
+
* `ready` the client (what it returns run as the game stops) and tells the card its controls can run
|
|
119
|
+
*/
|
|
120
|
+
function withExample(
|
|
121
|
+
view: GameView,
|
|
122
|
+
def: ExampleDef,
|
|
123
|
+
look: ExampleLook | undefined,
|
|
124
|
+
drawn: (client: GameClientApi, look: ExampleLook | undefined) => void
|
|
125
|
+
): GameView {
|
|
126
|
+
const own = look?.view ? look.view(view) : view
|
|
127
|
+
let told = false
|
|
128
|
+
return {
|
|
129
|
+
...own,
|
|
130
|
+
// the gold and lumber the RTS and MOBA examples play with are the one game's: shown in theirs alone
|
|
131
|
+
...(!RESOURCES_SHOWN.includes(def.category) && {
|
|
132
|
+
hud: { ...own.hud, widgets: [...(own.hud?.widgets ?? []), { id: 'resources', when: () => false }] }
|
|
133
|
+
}),
|
|
134
|
+
framePhases: [
|
|
135
|
+
...(own.framePhases ?? []),
|
|
136
|
+
{
|
|
137
|
+
name: 'examples.ready',
|
|
138
|
+
run: (_frame, client) => {
|
|
139
|
+
if (told) return
|
|
140
|
+
told = true
|
|
141
|
+
try {
|
|
142
|
+
const undo = look?.ready?.(client)
|
|
143
|
+
if (typeof undo === 'function') client.onStop(() => undo())
|
|
144
|
+
} catch (error) {
|
|
145
|
+
client.log.error('examples', 'The example failed as it was made ready', errorFields(error))
|
|
146
|
+
}
|
|
147
|
+
drawn(client, look)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The loading screen in the example's place: its name, a spinner, and a bar of how far its files are */
|
|
155
|
+
function loadingScreen(element: HTMLElement, title: string, progress: () => LoadProgress): () => void {
|
|
156
|
+
const screen = document.createElement('div')
|
|
157
|
+
screen.className = 'vx-loading'
|
|
158
|
+
screen.innerHTML = '<div class="vx-spinner"></div><strong></strong><div class="vx-bar"><div></div></div><span></span>'
|
|
159
|
+
screen.querySelector('strong')!.textContent = title
|
|
160
|
+
const bar = screen.querySelector<HTMLElement>('.vx-bar > div')!
|
|
161
|
+
const count = screen.querySelector('span')!
|
|
162
|
+
let frame = 0
|
|
163
|
+
const update = () => {
|
|
164
|
+
const { done, total } = progress()
|
|
165
|
+
bar.style.width = `${total ? Math.round((done / total) * 100) : 0}%`
|
|
166
|
+
count.textContent = total ? `${done} of ${total} files` : 'Starting…'
|
|
167
|
+
frame = requestAnimationFrame(update)
|
|
168
|
+
}
|
|
169
|
+
update()
|
|
170
|
+
element.append(screen)
|
|
171
|
+
return () => {
|
|
172
|
+
cancelAnimationFrame(frame)
|
|
173
|
+
screen.remove()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Every example's scene */
|
|
178
|
+
export const exampleScenes = (): Scene[] => examples.map(exampleScene)
|