@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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cinematics examples on the rules' side: their list entries, maps and starts, and the little the rules do for them.
|
|
3
|
+
* A cinematic is the screen's (`src/examples/cinematics/looks.ts` plays them): it reads the game and changes it only by
|
|
4
|
+
* commands, like the player. So the rules here are the commands a scene sends (walk there, fight), the units the
|
|
5
|
+
* scenes need, and one cinematic the rules play themselves, as a unit walks into a ring (`playCinematic`)
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
defineCommand,
|
|
9
|
+
defineComponent,
|
|
10
|
+
defineIds,
|
|
11
|
+
exactMath,
|
|
12
|
+
type Game,
|
|
13
|
+
id,
|
|
14
|
+
playCinematic,
|
|
15
|
+
point,
|
|
16
|
+
type UnitTypeDef,
|
|
17
|
+
type World
|
|
18
|
+
} from '@v12-engine/engine'
|
|
19
|
+
import { PLAYER, ENEMY, UNIT } from '../common/units'
|
|
20
|
+
import type { ExampleCategory } from '../define'
|
|
21
|
+
import { ROAD_END, SQUARE, villageMap } from './village'
|
|
22
|
+
|
|
23
|
+
/** The cinematics examples' own unit types */
|
|
24
|
+
export const CINE = defineIds('cinematics.raider', 'cinematics.villager')
|
|
25
|
+
|
|
26
|
+
const units: UnitTypeDef[] = [
|
|
27
|
+
{
|
|
28
|
+
// a raider of the scene: quick to fall, so that the fight is over in a few blows
|
|
29
|
+
id: CINE['cinematics.raider'],
|
|
30
|
+
name: 'Raider',
|
|
31
|
+
kind: 'unit',
|
|
32
|
+
radius: 0.45,
|
|
33
|
+
stats: { health: 110, attackDamage: 8, attackSpeed: 1.2, attackRange: 0.5, armor: 2, moveSpeed: 3.8 },
|
|
34
|
+
sight: 9,
|
|
35
|
+
attacks: [{ damagePoint: 0.45, acquisitionRange: 3 }]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
// someone of the village, standing about: what a flythrough flies over
|
|
39
|
+
id: CINE['cinematics.villager'],
|
|
40
|
+
name: 'Villager',
|
|
41
|
+
kind: 'unit',
|
|
42
|
+
radius: 0.4,
|
|
43
|
+
stats: { health: 150, moveSpeed: 2.6 },
|
|
44
|
+
sight: 8
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
/** What each example's map is called: its rules tell their own world by it (`world.map.name`) */
|
|
49
|
+
export const MAPS = { flythrough: 'Village', scene: 'Raid', screen: 'Square', trigger: 'Shrine', files: 'Tour', control: 'Transport' } as const
|
|
50
|
+
|
|
51
|
+
/** The ring of stones the trigger example's cinematic plays at, and the cinematic's id (the view's, `cinematics.defs`) */
|
|
52
|
+
export const SHRINE = { x: 24, z: 33, radius: 2.5 }
|
|
53
|
+
export const SHRINE_CINEMATIC = 'cinematics.shrine'
|
|
54
|
+
|
|
55
|
+
/** Villagers about the square, some at the fire, some at the market and the well */
|
|
56
|
+
function villagers(world: World): void {
|
|
57
|
+
const spots = [
|
|
58
|
+
[22, 24.5, 0.8],
|
|
59
|
+
[26, 24.6, -0.8],
|
|
60
|
+
[24, 28.3, Math.PI],
|
|
61
|
+
[20.5, 28.8, 2.2],
|
|
62
|
+
[28.3, 29.5, -1.2],
|
|
63
|
+
[29.6, 23.4, -0.6]
|
|
64
|
+
]
|
|
65
|
+
for (const [x, z, facing] of spots) world.spawn(CINE['cinematics.villager'], { at: { x, z }, owner: PLAYER, facing })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The raid's units: the knight at the end of the road, three raiders at the bonfire, the villagers fled */
|
|
69
|
+
function raid(world: World): void {
|
|
70
|
+
world.spawn(UNIT.knight, { at: { x: ROAD_END.x, z: ROAD_END.z + 4 }, owner: PLAYER, facing: 0, name: 'hero' })
|
|
71
|
+
const raiders = [
|
|
72
|
+
[SQUARE.x - 2, SQUARE.z - 3],
|
|
73
|
+
[SQUARE.x + 2, SQUARE.z - 3.4],
|
|
74
|
+
[SQUARE.x, SQUARE.z - 1.6]
|
|
75
|
+
]
|
|
76
|
+
raiders.forEach(([x, z], i) => world.spawn(CINE['cinematics.raider'], { at: { x, z }, owner: ENEMY, facing: Math.PI, name: `raider${i + 1}` }))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Takes every unit away */
|
|
80
|
+
function clear(world: World): void {
|
|
81
|
+
for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A scene's walk: a unit sent to a spot, whoever's it is (a cinematic directs everyone) */
|
|
85
|
+
export const walk = defineCommand('cinematics.walk', { unit: id, to: point }, (world, _player, command) => {
|
|
86
|
+
world.get(command.unit)?.order('move', command.to)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/** The raid's fight: the knight and the raiders at each other */
|
|
90
|
+
export const fight = defineCommand('cinematics.fight', {}, (world) => {
|
|
91
|
+
const hero = world.named('hero')
|
|
92
|
+
if (!hero?.alive) return
|
|
93
|
+
for (const unit of world.units.values()) {
|
|
94
|
+
if (!unit.alive || unit.def.id !== CINE['cinematics.raider']) continue
|
|
95
|
+
unit.order('attack', hero)
|
|
96
|
+
}
|
|
97
|
+
const first = [...world.units.values()].find((unit) => unit.alive && unit.def.id === CINE['cinematics.raider'])
|
|
98
|
+
if (first) hero.order('attack', first)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
/** Lays the raid out again, as at the start */
|
|
102
|
+
export const resetRaid = defineCommand('cinematics.resetRaid', {}, (world) => {
|
|
103
|
+
clear(world)
|
|
104
|
+
raid(world)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/** Whether the shrine's cinematic has played since the knight last stood back on the road */
|
|
108
|
+
const shrineSeen = defineComponent<{ seen: boolean }>({ id: 'cinematics.shrineSeen', default: () => ({ seen: false }) })
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The trigger: the first time the knight walks into the ring of stones, the rules play the shrine's cinematic for
|
|
112
|
+
* its owner, the game held while it plays. Every map plays this game, so it checks the map is the trigger example's.
|
|
113
|
+
* (A region of a map can say the same with no code: `MapRegion.cinematic`)
|
|
114
|
+
*/
|
|
115
|
+
const shrineTrigger = {
|
|
116
|
+
name: 'cinematics.shrineTrigger',
|
|
117
|
+
every: 3,
|
|
118
|
+
update(world: World) {
|
|
119
|
+
if (world.map.name !== MAPS.trigger || world.state(shrineSeen).seen) return
|
|
120
|
+
const hero = world.named('hero')
|
|
121
|
+
if (!hero?.alive || exactMath.hypot(hero.pos.x - SHRINE.x, hero.pos.z - SHRINE.z) > SHRINE.radius) return
|
|
122
|
+
world.setState(shrineSeen, { seen: true })
|
|
123
|
+
hero.order('stop')
|
|
124
|
+
playCinematic(world, SHRINE_CINEMATIC, { to: hero.owner, hold: true, params: { hero: hero.id } })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The knight back on the road, the shrine ready to play again */
|
|
129
|
+
export const resetShrine = defineCommand('cinematics.resetShrine', {}, (world) => {
|
|
130
|
+
world.setState(shrineSeen, { seen: false })
|
|
131
|
+
const hero = world.named('hero')
|
|
132
|
+
if (!hero?.alive) return
|
|
133
|
+
hero.order('stop')
|
|
134
|
+
hero.moveTo(ROAD_END.x, ROAD_END.z + 6)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
export const category: ExampleCategory = {
|
|
138
|
+
rules: (game: Game<any>) => {
|
|
139
|
+
for (const unit of units) game.unit(unit)
|
|
140
|
+
game.component(shrineSeen)
|
|
141
|
+
game.system(shrineTrigger)
|
|
142
|
+
game.command(walk)
|
|
143
|
+
game.command(fight)
|
|
144
|
+
game.command(resetRaid)
|
|
145
|
+
game.command(resetShrine)
|
|
146
|
+
},
|
|
147
|
+
examples: [
|
|
148
|
+
{
|
|
149
|
+
id: 'cinematics-flythrough',
|
|
150
|
+
category: 'cinematics',
|
|
151
|
+
title: 'A flythrough',
|
|
152
|
+
summary:
|
|
153
|
+
'The camera flies over the village in one smooth move through a few shots, along the road on a rail, or round the bonfire. Press a button to fly; Esc skips.',
|
|
154
|
+
tags: ['camera', 'shot', 'rail', 'path', 'orbit', 'flyover', 'cut.shot', 'cut.rail', 'cut.orbit'],
|
|
155
|
+
map: () => villageMap(MAPS.flythrough, 1),
|
|
156
|
+
start: (world) => villagers(world),
|
|
157
|
+
source: ['examples/cinematics/index.ts', 'examples/cinematics/looks.ts', 'examples/cinematics/village.ts']
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'cinematics-scene',
|
|
161
|
+
category: 'cinematics',
|
|
162
|
+
title: 'A scene with actors',
|
|
163
|
+
summary:
|
|
164
|
+
'A knight walks up the road, raiders at the bonfire taunt him, he answers, and they fight: units given orders by the cinematic, lines said, and the camera cutting between them.',
|
|
165
|
+
tags: ['timeline', 'actors', 'walk', 'fight', 'talk', 'camera cuts', 'follow', 'say', 'commands'],
|
|
166
|
+
map: () => villageMap(MAPS.scene),
|
|
167
|
+
start: (world) => raid(world),
|
|
168
|
+
source: ['examples/cinematics/index.ts', 'examples/cinematics/looks.ts']
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'cinematics-screen',
|
|
172
|
+
category: 'cinematics',
|
|
173
|
+
title: 'Bars, fades and titles',
|
|
174
|
+
summary:
|
|
175
|
+
'What a cinematic does to the frame: letterbox bars, a fade to black, a title card, a vignette, a cold grade, slow motion and transitions between shots. Each button plays a short one.',
|
|
176
|
+
tags: ['letterbox', 'fade', 'title card', 'vignette', 'grade', 'transition', 'iris', 'wipe', 'dissolve', 'slow motion', 'screen'],
|
|
177
|
+
map: () => villageMap(MAPS.screen, 1),
|
|
178
|
+
start: (world) => villagers(world),
|
|
179
|
+
source: ['examples/cinematics/index.ts', 'examples/cinematics/looks.ts']
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'cinematics-trigger',
|
|
183
|
+
category: 'cinematics',
|
|
184
|
+
title: 'Walk in, watch, play on',
|
|
185
|
+
summary:
|
|
186
|
+
'Walk the knight (selected) into the ring of stones north of the fire: the rules play a cinematic, the game held, and give the player the knight back after. Again puts him on the road.',
|
|
187
|
+
tags: ['trigger', 'region', 'playCinematic', 'hold', 'gameplay', 'return control', 'area'],
|
|
188
|
+
map: () => villageMap(MAPS.trigger, 1),
|
|
189
|
+
start: (world) => {
|
|
190
|
+
world.spawn(UNIT.knight, { at: { x: ROAD_END.x, z: ROAD_END.z + 6 }, owner: PLAYER, facing: 0, name: 'hero' })
|
|
191
|
+
villagers(world)
|
|
192
|
+
},
|
|
193
|
+
source: ['examples/cinematics/index.ts', 'examples/cinematics/looks.ts']
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'cinematics-files',
|
|
197
|
+
category: 'cinematics',
|
|
198
|
+
title: 'From a file, or in code',
|
|
199
|
+
summary:
|
|
200
|
+
'One tour of the village made twice: as data, a file the editor’s Cinematic panel writes (lanes of keys and clips), and as a script, an async function given a cut. Play either; they look alike.',
|
|
201
|
+
tags: ['json', 'file', 'readCinematic', 'CinematicDef', 'lanes', 'script', 'code', 'editor'],
|
|
202
|
+
map: () => villageMap(MAPS.files, 1),
|
|
203
|
+
start: (world) => villagers(world),
|
|
204
|
+
source: ['examples/cinematics/index.ts', 'examples/cinematics/looks.ts', 'examples/cinematics/tour.json']
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'cinematics-control',
|
|
208
|
+
category: 'cinematics',
|
|
209
|
+
title: 'Pause, seek, skip',
|
|
210
|
+
summary: 'A cinematic on its own clock: pause it, jump to a moment, slow it down, skip it (or make it unskippable), and read how it ended.',
|
|
211
|
+
tags: ['skip', 'pause', 'seek', 'rate', 'clock', 'Playing', 'outcome', 'skippable', 'Esc'],
|
|
212
|
+
map: () => villageMap(MAPS.control, 1),
|
|
213
|
+
start: (world) => villagers(world),
|
|
214
|
+
source: ['examples/cinematics/index.ts', 'examples/cinematics/looks.ts']
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cinematics examples on the client's side: the scripts they play, their controls, and the looks of their units.
|
|
3
|
+
* A cinematic here is mostly code: an `async` function given a cut (`Cinematic`), played with `client.play` or
|
|
4
|
+
* `client.cinematics.play` (which gives back a `Playing` to pause, seek and skip). One is a file (`./tour.json`), as the
|
|
5
|
+
* editor's Cinematic panel writes them, read with `readCinematic`. The rules play one themselves (the shrine's), by
|
|
6
|
+
* its id in the view (`cinematics.defs`)
|
|
7
|
+
*/
|
|
8
|
+
import { type Cinematic, type Cut, type GameClientApi, type GameView, mountTitleCard, type Playing, readCinematic } from '@v12-engine/client'
|
|
9
|
+
import { button, type CategoryLooks, readout, slider, toggle } from '../../shell/look'
|
|
10
|
+
import { character, hold, ONE_HANDED, person } from '../common/looks'
|
|
11
|
+
import { CINE, fight, resetRaid, resetShrine, SHRINE, SHRINE_CINEMATIC, walk } from './index'
|
|
12
|
+
import { ROAD_END, SQUARE } from './village'
|
|
13
|
+
import tourFile from './tour.json'
|
|
14
|
+
|
|
15
|
+
/** Who speaks in the scenes: their names, colours and faces (`GameView.cinematics.speakers`) */
|
|
16
|
+
const SPEAKERS = {
|
|
17
|
+
'cinematics.knight': { name: 'Sir Aldwin', color: '#9cc4ff', portrait: { type: 'knight' } },
|
|
18
|
+
'cinematics.raider': { name: 'Raider chief', color: '#ff8a65', portrait: { type: CINE['cinematics.raider'] } },
|
|
19
|
+
'cinematics.narrator': { name: 'Narrator', color: '#e8d9a8' }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The tour of the village as data: a file of lanes, the editor's (`readCinematic` checks it and says what is wrong) */
|
|
23
|
+
const tour = readCinematic(tourFile)
|
|
24
|
+
|
|
25
|
+
/** A title card over the picture for `seconds`, on the cut's clock (it pauses and slows with it) */
|
|
26
|
+
function titleCard(cut: Cut, title: string, subtitle: string, seconds = 3.5, style = 'title'): void {
|
|
27
|
+
cut.ui.mount({ mount: (element) => mountTitleCard(element, { title, subtitle, style, for: seconds, in: 0.6, out: 0.8 }, cut.timers) })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A pose of the camera: looking at a spot of the map from `distance`, turned `yaw` round it, `pitch` down to it */
|
|
31
|
+
const view = (x: number, z: number, distance: number, yaw: number, pitch: number, fov?: number) => ({
|
|
32
|
+
at: { x, z },
|
|
33
|
+
distance,
|
|
34
|
+
yaw,
|
|
35
|
+
pitch,
|
|
36
|
+
...(fov && { fov })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ---- A flythrough
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* In one move through five shots: in over the woods, down to the square, round the well, up and away. The move's time
|
|
43
|
+
* is shared out by how far apart the shots' spots on the ground are, so no two of them look at the same spot
|
|
44
|
+
*/
|
|
45
|
+
const flythrough: Cinematic = async (cut) => {
|
|
46
|
+
await cut.camera(view(24, 14, 44, 0, 0.8), 0)
|
|
47
|
+
titleCard(cut, 'Elmbrook', 'a village of the north road', 4)
|
|
48
|
+
await cut.shot(
|
|
49
|
+
[
|
|
50
|
+
view(24, 17, 30, 0.1, 0.6),
|
|
51
|
+
view(24, 25, 14, 0.25, 0.38, 50),
|
|
52
|
+
view(27, 28, 12, 1.4, 0.32, 50),
|
|
53
|
+
view(23, 25, 24, 3.2, 0.55),
|
|
54
|
+
view(25, 20, 48, 4.4, 0.95)
|
|
55
|
+
],
|
|
56
|
+
{ seconds: 16, ease: 'inOutSine' }
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Along the road on a rail (the map's path `road`), the camera turned the way it goes */
|
|
61
|
+
const rail: Cinematic = async (cut) => {
|
|
62
|
+
await cut.camera(view(24, 2, 14, 0, 0.45), 0)
|
|
63
|
+
await cut.rail('road', { seconds: 12, distance: 14, pitch: 0.45, yaw: 'along', smooth: 4, ease: 'inOutSine' })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Round the bonfire, a full turn, the lens long */
|
|
67
|
+
const orbit: Cinematic = async (cut) => {
|
|
68
|
+
await cut.camera(view(SQUARE.x, SQUARE.z, 18, 0, 0.42, 45), 0.8)
|
|
69
|
+
await cut.orbit(SQUARE, { radians: Math.PI * 2, seconds: 12 })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- A scene with actors
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The raid: the knight walks up the road (an order, sent as a command), the camera following; the raiders' chief
|
|
76
|
+
* taunts him, a cut to each face; then the fight, the camera framing them all. The cinematic directs both sides with
|
|
77
|
+
* the example's own commands (`./index.ts`): a game with the director pack (`directorPack`) can use the units lane's
|
|
78
|
+
*/
|
|
79
|
+
const raidScene =
|
|
80
|
+
(client: GameClientApi): Cinematic =>
|
|
81
|
+
async (cut) => {
|
|
82
|
+
const hero = cut.entity('hero')
|
|
83
|
+
const chief = cut.entity('raider3')
|
|
84
|
+
if (hero === undefined || chief === undefined) return
|
|
85
|
+
// the establishing shot: the raiders at the fire, from over the road
|
|
86
|
+
await cut.camera(view(SQUARE.x, SQUARE.z - 4, 22, 0.3, 0.5), 0)
|
|
87
|
+
titleCard(cut, 'The Raid', 'Elmbrook, at dusk', 3, 'chapter')
|
|
88
|
+
await cut.shot(view(SQUARE.x, SQUARE.z - 2, 15, 0.1, 0.42), { seconds: 3, ease: 'inOutCubic' })
|
|
89
|
+
// a cut to the road: the knight walks in, the camera following him
|
|
90
|
+
await cut.shot(view(ROAD_END.x, ROAD_END.z + 6, 10, 2.8, 0.35), { transition: 'dissolve' })
|
|
91
|
+
client.session.send(walk.make({ unit: hero, to: { x: SQUARE.x, z: SQUARE.z - 9 } }))
|
|
92
|
+
cut.follow(hero, { distance: 11, pitch: 0.35, lag: 0.4 })
|
|
93
|
+
await cut.say('cinematics.knight', 'Smoke over Elmbrook. I should have come sooner.', 3.5)
|
|
94
|
+
await cut.until(() => {
|
|
95
|
+
const at = cut.where(hero)
|
|
96
|
+
return !!at && at.z > SQUARE.z - 9.5
|
|
97
|
+
}, 8)
|
|
98
|
+
// he stands his ground while they talk: on hold, he swings at what comes within reach but chases nothing
|
|
99
|
+
client.session.send({ type: 'order', entities: [hero], order: { type: 'hold' } })
|
|
100
|
+
// the chief, close: a long lens over the knight's shoulder
|
|
101
|
+
await cut.shot({ at: { entity: chief }, distance: 7, yaw: 0.25, pitch: 0.18, fov: 40, height: 1.4 }, { seconds: 0.6 })
|
|
102
|
+
await cut.say('cinematics.raider', 'One knight? Turn round, tin man, and we let you keep your horse.', 4)
|
|
103
|
+
await cut.shot({ at: { entity: hero }, distance: 6, yaw: Math.PI - 0.25, pitch: 0.15, fov: 40, height: 1.6 })
|
|
104
|
+
await cut.say('cinematics.knight', 'I have no horse.', 2.5)
|
|
105
|
+
// the fight: every one of them in the picture, the camera shaking with the first blows
|
|
106
|
+
client.session.send(fight.make({}))
|
|
107
|
+
await cut.fit(['hero', 'raider1', 'raider2', 'raider3'], { seconds: 0.8 })
|
|
108
|
+
cut.follow(['hero', 'raider1', 'raider2', 'raider3'], { fit: { padding: 5 }, pitch: 0.55, lag: 0.5 })
|
|
109
|
+
await cut.wait(1.2)
|
|
110
|
+
cut.shake(0.4)
|
|
111
|
+
await cut.until(() => [1, 2, 3].every((n) => cut.entity(`raider${n}`) === undefined), 14)
|
|
112
|
+
await cut.shot({ at: { entity: hero }, distance: 9, yaw: 0.6, pitch: 0.3 }, { seconds: 1.2, ease: 'inOutCubic' })
|
|
113
|
+
await cut.say('cinematics.knight', 'Elmbrook is free. Ring the bell.', 3)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- Bars, fades and titles
|
|
117
|
+
|
|
118
|
+
/** Each a short cinematic of one thing the frame does, over the same shot of the square */
|
|
119
|
+
const SQUARE_SHOT = view(SQUARE.x, SQUARE.z, 20, 0.5, 0.5)
|
|
120
|
+
const screenCuts: Record<string, Cinematic> = {
|
|
121
|
+
letterbox: async (cut) => {
|
|
122
|
+
await cut.camera(SQUARE_SHOT, 0)
|
|
123
|
+
await cut.screen.letterbox(2.39, 0.8)
|
|
124
|
+
await cut.shot(view(SQUARE.x, SQUARE.z, 16, 1.1, 0.4), { seconds: 4 })
|
|
125
|
+
await cut.screen.letterbox(false, 0.8)
|
|
126
|
+
},
|
|
127
|
+
fade: async (cut) => {
|
|
128
|
+
await cut.camera(SQUARE_SHOT, 0)
|
|
129
|
+
await cut.wait(0.6)
|
|
130
|
+
await cut.screen.fade('#000', 1, 1.2)
|
|
131
|
+
await cut.camera(view(29, 28.5, 9, 2.4, 0.35), 0)
|
|
132
|
+
await cut.wait(0.6)
|
|
133
|
+
await cut.screen.fade('#000', 0, 1.2)
|
|
134
|
+
await cut.wait(1.5)
|
|
135
|
+
},
|
|
136
|
+
title: async (cut) => {
|
|
137
|
+
await cut.camera(SQUARE_SHOT, 0)
|
|
138
|
+
await cut.screen.vignette(0.6, 0.5, 0.6)
|
|
139
|
+
titleCard(cut, 'Chapter One', 'The North Road', 4, 'chapter')
|
|
140
|
+
await cut.wait(4.3)
|
|
141
|
+
titleCard(cut, 'Elmbrook', 'three days from the capital', 3.5, 'lower-third')
|
|
142
|
+
await cut.wait(3.8)
|
|
143
|
+
},
|
|
144
|
+
grade: async (cut) => {
|
|
145
|
+
await cut.camera(SQUARE_SHOT, 0)
|
|
146
|
+
await cut.picture.grade({ saturation: 0.35, contrast: 1.15, tint: [0.55, 0.7, 1], tintAmount: 0.35 }, 1.5)
|
|
147
|
+
await cut.screen.vignette(0.7, 0.5, 1)
|
|
148
|
+
await cut.shot(view(SQUARE.x, SQUARE.z, 14, 1.4, 0.35), { seconds: 3 })
|
|
149
|
+
await cut.picture.reset(1)
|
|
150
|
+
},
|
|
151
|
+
slow: async (cut) => {
|
|
152
|
+
await cut.camera(view(SQUARE.x, SQUARE.z, 11, 0.4, 0.3, 45), 0)
|
|
153
|
+
await cut.screen.letterbox(2.39, 0.4)
|
|
154
|
+
await cut.speed.slow(0.2, 0.6)
|
|
155
|
+
await cut.screen.tint('#ffd9a0', 0.15, 0.6)
|
|
156
|
+
await cut.shot(view(SQUARE.x, SQUARE.z, 9, 1.3, 0.25, 45), { seconds: 4 })
|
|
157
|
+
cut.speed.reset()
|
|
158
|
+
},
|
|
159
|
+
transitions: async (cut) => {
|
|
160
|
+
const shots = [
|
|
161
|
+
view(24, 20, 20, 0.3, 0.5),
|
|
162
|
+
view(15, 33, 14, 2.4, 0.35),
|
|
163
|
+
view(29, 28.5, 9, 2.4, 0.35),
|
|
164
|
+
view(34, 20, 16, 4.5, 0.4),
|
|
165
|
+
view(24, 40, 18, 3.1, 0.3)
|
|
166
|
+
]
|
|
167
|
+
const ways = ['dissolve', 'wipe', 'iris', 'push', 'zoom']
|
|
168
|
+
await cut.camera(SQUARE_SHOT, 0)
|
|
169
|
+
for (let i = 0; i < shots.length; i++) {
|
|
170
|
+
await cut.wait(0.9)
|
|
171
|
+
titleCard(cut, ways[i], '', 1.6, 'lower-third')
|
|
172
|
+
await cut.shot(shots[i], { transition: { id: ways[i], seconds: 0.8 } })
|
|
173
|
+
}
|
|
174
|
+
await cut.wait(1.4)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- Walk in, watch, play on
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The shrine's cinematic, played by the rules as the knight walks into the ring (`playCinematic`, handed his id): the
|
|
182
|
+
* camera round him, a voice from the stones, and the bars away. Then the player has him back, where he stood
|
|
183
|
+
*/
|
|
184
|
+
const shrine: Cinematic = async (cut, params) => {
|
|
185
|
+
const hero = Number(params?.hero)
|
|
186
|
+
await cut.shot({ at: SHRINE, distance: 12, yaw: 0.4, pitch: 0.35 }, { seconds: 1.2, ease: 'inOutCubic' })
|
|
187
|
+
cut.fx('spark-burst', { at: SHRINE })
|
|
188
|
+
await cut.screen.flash('#fff6d0', 0.4)
|
|
189
|
+
await cut.say('cinematics.narrator', 'The stones hum. Something old is awake beneath Elmbrook.', 3.5)
|
|
190
|
+
await cut.orbit({ entity: hero }, { radians: Math.PI * 0.8, seconds: 4 })
|
|
191
|
+
await cut.say('cinematics.knight', 'Then I will be ready for it.', 2.5)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---- Pause, seek, skip
|
|
195
|
+
|
|
196
|
+
/** What the transport example is playing, and how the last one ended */
|
|
197
|
+
const transport: { playing?: Playing; outcome?: string; skippable: boolean; rate: number } = { skippable: true, rate: 1 }
|
|
198
|
+
|
|
199
|
+
/** Plays the tour (the file), keeping hold of it for the controls */
|
|
200
|
+
function playTour(client: GameClientApi): void {
|
|
201
|
+
transport.playing?.stop()
|
|
202
|
+
transport.outcome = undefined
|
|
203
|
+
const playing = client.cinematics.play(tour, { skippable: transport.skippable, rate: transport.rate })
|
|
204
|
+
transport.playing = playing
|
|
205
|
+
void playing.done.then((outcome) => {
|
|
206
|
+
transport.outcome = outcome
|
|
207
|
+
if (transport.playing === playing) transport.playing = undefined
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The tour in code, the same shots and lines as the file: what the file's lanes do, said as steps */
|
|
212
|
+
const tourInCode: Cinematic = async (cut) => {
|
|
213
|
+
await cut.screen.letterbox(2.39, 0.6)
|
|
214
|
+
await cut.camera(view(24, 6, 40, 0, 0.8), 0)
|
|
215
|
+
cut.screen.fade('#000', 0, 1.5)
|
|
216
|
+
titleCard(cut, 'Elmbrook', 'made in code', 4)
|
|
217
|
+
void cut.say('cinematics.narrator', 'In the north there is a village where the road ends.', 4.5)
|
|
218
|
+
await cut.shot(view(24, 22, 20, 0.6, 0.5), { seconds: 6, ease: 'inOutCubic' })
|
|
219
|
+
void cut.say('cinematics.narrator', 'Its people keep the fire lit, and the bell rung.', 4.5)
|
|
220
|
+
await cut.shot(view(24, 30, 18, 2.8, 0.45), { seconds: 6, ease: 'inOutSine' })
|
|
221
|
+
await cut.shot(view(24, 26, 42, 3.6, 0.9), { seconds: 4, ease: 'inOutSine' })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The player's camera over a spot, `distance` away, turned and tilted as it is: where an example starts */
|
|
225
|
+
export function aim(client: GameClientApi, at: { x: number; z: number }, distance: number): void {
|
|
226
|
+
const { yaw, pitch } = client.camera.pose()
|
|
227
|
+
client.camera.setPose({ x: at.x, z: at.z, distance, yaw, pitch })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The village alive: the bonfire burning, smoke from two chimneys. Gives back what puts them out */
|
|
231
|
+
export function villageFx(client: GameClientApi): () => void {
|
|
232
|
+
const lit = [
|
|
233
|
+
client.fx.play('campfire', { at: SQUARE }),
|
|
234
|
+
client.fx.play('chimney', { at: { x: 15.6, y: 6, z: 34.2 } }),
|
|
235
|
+
client.fx.play('chimney', { at: { x: 13.6, y: 6.5, z: 20.4 } })
|
|
236
|
+
]
|
|
237
|
+
return () => lit.forEach((each) => each.stop({ clear: true }))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Plays a cinematic in the place of any playing now (`replaced`): what the example's buttons all do. The screen is made
|
|
242
|
+
* clear first: one replaced half way through its fade to black leaves the fade where it got to (a cinematic's fade
|
|
243
|
+
* stays after it, for a game that ends on black), and the next would play in the dark
|
|
244
|
+
*/
|
|
245
|
+
const play = (client: GameClientApi, what: Cinematic) =>
|
|
246
|
+
void client.play(async (cut, params) => {
|
|
247
|
+
await cut.screen.reset(0)
|
|
248
|
+
await what(cut, params)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
export const category: CategoryLooks = {
|
|
252
|
+
// the units of the scenes, and who speaks in them
|
|
253
|
+
view: (view: GameView) => ({
|
|
254
|
+
...view,
|
|
255
|
+
units: {
|
|
256
|
+
...view.units,
|
|
257
|
+
looks: {
|
|
258
|
+
...view.units.looks,
|
|
259
|
+
[CINE['cinematics.raider']]: {
|
|
260
|
+
height: 1.8,
|
|
261
|
+
model: character('barbarian', 0.7, { hold: [hold('axe_1handed'), hold('shield_round_barbarian', 'l')] }),
|
|
262
|
+
attacks: [ONE_HANDED],
|
|
263
|
+
shapes: person(1.8)
|
|
264
|
+
},
|
|
265
|
+
[CINE['cinematics.villager']]: { height: 1.7, model: character('rogue_hooded', 0.66), shapes: person(1.7, [0.55, 0.45, 0.35]) }
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
cinematics: {
|
|
269
|
+
...view.cinematics,
|
|
270
|
+
// the one the rules play by its id (`playCinematic`)
|
|
271
|
+
defs: { ...view.cinematics?.defs, [SHRINE_CINEMATIC]: { play: shrine, back: true } },
|
|
272
|
+
speakers: { ...view.cinematics?.speakers, ...SPEAKERS }
|
|
273
|
+
}
|
|
274
|
+
}),
|
|
275
|
+
looks: {
|
|
276
|
+
'cinematics-flythrough': {
|
|
277
|
+
controls: [
|
|
278
|
+
button(
|
|
279
|
+
'Fly through the village',
|
|
280
|
+
(client) => play(client, flythrough),
|
|
281
|
+
'Plays a fly-through of the village: five camera shots joined in one smooth move (cut.shot), the bars in at the edges.'
|
|
282
|
+
),
|
|
283
|
+
button(
|
|
284
|
+
'Down the road on a rail',
|
|
285
|
+
(client) => play(client, rail),
|
|
286
|
+
'The camera glides down the village road along a path drawn on the map, “road”, turned the way it goes (cut.rail).'
|
|
287
|
+
),
|
|
288
|
+
button(
|
|
289
|
+
'Round the fire',
|
|
290
|
+
(client) => play(client, orbit),
|
|
291
|
+
'The camera makes a full slow turn round the bonfire with a long lens (cut.orbit).'
|
|
292
|
+
),
|
|
293
|
+
button('Skip', (client) => client.skipCinematic(), 'Ends the cinematic playing now, as Esc does: the camera is the player’s again.')
|
|
294
|
+
],
|
|
295
|
+
ready: (client) => {
|
|
296
|
+
aim(client, SQUARE, 34)
|
|
297
|
+
const out = villageFx(client)
|
|
298
|
+
const timer = setTimeout(() => play(client, flythrough), 600)
|
|
299
|
+
return () => {
|
|
300
|
+
clearTimeout(timer)
|
|
301
|
+
out()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
'cinematics-scene': {
|
|
306
|
+
controls: [
|
|
307
|
+
button(
|
|
308
|
+
'Play the scene',
|
|
309
|
+
(client) => {
|
|
310
|
+
client.session.send(resetRaid.make({}))
|
|
311
|
+
// the units laid out again land at the next tick: the scene starts after them
|
|
312
|
+
setTimeout(() => play(client, raidScene(client)), 150)
|
|
313
|
+
},
|
|
314
|
+
'Lays the raiders and the knight out again and plays the scene: he walks in, the chief taunts him with a cut to each face, then the fight.'
|
|
315
|
+
),
|
|
316
|
+
button('Skip', (client) => client.skipCinematic(), 'Ends the scene now, as Esc does: the units stay where the scene left them.')
|
|
317
|
+
],
|
|
318
|
+
ready: (client) => {
|
|
319
|
+
aim(client, { x: SQUARE.x, z: SQUARE.z - 6 }, 30)
|
|
320
|
+
const out = villageFx(client)
|
|
321
|
+
const timer = setTimeout(() => play(client, raidScene(client)), 600)
|
|
322
|
+
return () => {
|
|
323
|
+
clearTimeout(timer)
|
|
324
|
+
out()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
'cinematics-screen': {
|
|
329
|
+
controls: [
|
|
330
|
+
button(
|
|
331
|
+
'Letterbox',
|
|
332
|
+
(client) => play(client, screenCuts.letterbox),
|
|
333
|
+
'Black bars slide in to a 2.39:1 frame, then out again (cut.screen.letterbox).'
|
|
334
|
+
),
|
|
335
|
+
button(
|
|
336
|
+
'Fade to black',
|
|
337
|
+
(client) => play(client, screenCuts.fade),
|
|
338
|
+
'The picture fades out to black, the camera cuts in the dark, and it fades back in (cut.screen.fade).'
|
|
339
|
+
),
|
|
340
|
+
button(
|
|
341
|
+
'Title cards',
|
|
342
|
+
(client) => play(client, screenCuts.title),
|
|
343
|
+
'A chapter title over the picture, then a smaller name in the lower third (mountTitleCard).'
|
|
344
|
+
),
|
|
345
|
+
button(
|
|
346
|
+
'Cold grade',
|
|
347
|
+
(client) => play(client, screenCuts.grade),
|
|
348
|
+
'The picture’s colour drained and cooled to blue for a few seconds, then back (cut.picture.grade).'
|
|
349
|
+
),
|
|
350
|
+
button(
|
|
351
|
+
'Slow motion',
|
|
352
|
+
(client) => play(client, screenCuts.slow),
|
|
353
|
+
'The whole world slows to a fifth of its speed, the fire and smoke with it, then back (cut.speed.slow).'
|
|
354
|
+
),
|
|
355
|
+
button(
|
|
356
|
+
'Transitions',
|
|
357
|
+
(client) => play(client, screenCuts.transitions),
|
|
358
|
+
'Five cuts between two shots, each a different transition: dissolve, wipe, iris, push and zoom.'
|
|
359
|
+
)
|
|
360
|
+
],
|
|
361
|
+
ready: (client) => {
|
|
362
|
+
aim(client, SQUARE, 26)
|
|
363
|
+
return villageFx(client)
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
'cinematics-trigger': {
|
|
367
|
+
controls: [
|
|
368
|
+
button(
|
|
369
|
+
'Walk him in',
|
|
370
|
+
(client) => {
|
|
371
|
+
const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
|
|
372
|
+
if (hero) client.session.send(walk.make({ unit: hero.id, to: SHRINE }))
|
|
373
|
+
},
|
|
374
|
+
'Walks the knight into the ring of stones: the rules see him arrive and play the shrine’s cinematic. You can also order him there yourself.'
|
|
375
|
+
),
|
|
376
|
+
button(
|
|
377
|
+
'Again',
|
|
378
|
+
(client) => client.session.send(resetShrine.make({})),
|
|
379
|
+
'Puts the knight back on the road and resets the stones, so walking in plays the cinematic again.'
|
|
380
|
+
),
|
|
381
|
+
readout(
|
|
382
|
+
'Playing',
|
|
383
|
+
(client) => (client.cinematics.current ? `${client.cinematics.current.id ?? 'a cinematic'}` : 'the game'),
|
|
384
|
+
'The cinematic playing now, by its id, or “the game” when the player has the camera.'
|
|
385
|
+
)
|
|
386
|
+
],
|
|
387
|
+
ready: (client) => {
|
|
388
|
+
aim(client, { x: 24, z: 20 }, 32)
|
|
389
|
+
const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
|
|
390
|
+
if (hero) client.controller.select([hero.id])
|
|
391
|
+
// the ring to walk into: a label over the stones, pinned to the world
|
|
392
|
+
const label = document.createElement('div')
|
|
393
|
+
label.textContent = '◈ The old stones'
|
|
394
|
+
label.style.cssText =
|
|
395
|
+
'font:600 13px Georgia,serif;color:#ffe9a8;padding:3px 9px;border-radius:12px;background:rgba(20,14,6,.7);border:1px solid #c9a35a;white-space:nowrap'
|
|
396
|
+
const pin = client.attach(label, { x: SHRINE.x, z: SHRINE.z, height: 1 })
|
|
397
|
+
const glow = client.fx.play('glow', { at: SHRINE })
|
|
398
|
+
const out = villageFx(client)
|
|
399
|
+
return () => {
|
|
400
|
+
pin.remove()
|
|
401
|
+
glow.stop({ clear: true })
|
|
402
|
+
out()
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
'cinematics-files': {
|
|
407
|
+
controls: [
|
|
408
|
+
button(
|
|
409
|
+
'Play the file',
|
|
410
|
+
(client) => void client.play(tour),
|
|
411
|
+
'Plays tour.json, a cinematic made as data (as the editor saves it): lanes of camera keys, the bars, spoken lines and a title.'
|
|
412
|
+
),
|
|
413
|
+
button(
|
|
414
|
+
'Play the code',
|
|
415
|
+
(client) => play(client, tourInCode),
|
|
416
|
+
'Plays the same tour written as code, step after step: compare it with the file.'
|
|
417
|
+
),
|
|
418
|
+
button('Skip', (client) => client.skipCinematic(), 'Ends the tour now, as Esc does.')
|
|
419
|
+
],
|
|
420
|
+
ready: (client) => {
|
|
421
|
+
aim(client, SQUARE, 30)
|
|
422
|
+
return villageFx(client)
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
'cinematics-control': {
|
|
426
|
+
controls: [
|
|
427
|
+
button('Play', (client) => playTour(client), 'Plays the village tour from the start, at the speed and skippable as set below.'),
|
|
428
|
+
button('Pause', () => transport.playing?.pause(), 'Holds the tour where it is: the camera, the lines and the clock stop.'),
|
|
429
|
+
button('Resume', () => transport.playing?.resume(), 'Goes on with the tour from where it was paused.'),
|
|
430
|
+
button(
|
|
431
|
+
'Jump to 9 s',
|
|
432
|
+
() => transport.playing?.seek(9),
|
|
433
|
+
'Jumps the tour to 9 seconds in: the camera, the bars and the lines are as they are at that moment.'
|
|
434
|
+
),
|
|
435
|
+
slider(
|
|
436
|
+
'Speed',
|
|
437
|
+
{ min: 0.25, max: 2, step: 0.25, value: 1 },
|
|
438
|
+
(_client, value) => {
|
|
439
|
+
transport.rate = value
|
|
440
|
+
transport.playing?.rate(value)
|
|
441
|
+
},
|
|
442
|
+
'How fast the tour plays, from a quarter to twice: the camera moves and the lines follow its clock.'
|
|
443
|
+
),
|
|
444
|
+
toggle(
|
|
445
|
+
'Skippable',
|
|
446
|
+
true,
|
|
447
|
+
(_client, on) => (transport.skippable = on),
|
|
448
|
+
'Whether Esc and Skip may end the tour: off, they do nothing, from the next Play on.'
|
|
449
|
+
),
|
|
450
|
+
button(
|
|
451
|
+
'Skip',
|
|
452
|
+
(client) => client.skipCinematic(),
|
|
453
|
+
'Ends the tour now, as Esc does (in code, playing.stop()): does nothing while Skippable is off.'
|
|
454
|
+
),
|
|
455
|
+
readout(
|
|
456
|
+
'Clock',
|
|
457
|
+
() => {
|
|
458
|
+
const { playing } = transport
|
|
459
|
+
if (playing) return `${playing.cut.time.toFixed(1)} s${playing.paused ? ', paused' : ''}`
|
|
460
|
+
return transport.outcome ? `over: ${transport.outcome}` : 'not playing'
|
|
461
|
+
},
|
|
462
|
+
'Where the tour is on its own clock, and how the last one ended: ended, skipped, stopped or replaced.'
|
|
463
|
+
)
|
|
464
|
+
],
|
|
465
|
+
ready: (client) => {
|
|
466
|
+
aim(client, SQUARE, 30)
|
|
467
|
+
const out = villageFx(client)
|
|
468
|
+
return () => {
|
|
469
|
+
transport.playing = undefined
|
|
470
|
+
transport.outcome = undefined
|
|
471
|
+
out()
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|