@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,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A meteor, on the rules' side: footmen standing about a field for it to fall among. The meteor is the view's effect
|
|
3
|
+
* (`./meteor.look.ts`); the rules could call one down for every player with `world.fx('customFx.meteor', { at })`
|
|
4
|
+
*/
|
|
5
|
+
import { exactMath } from '@v12-engine/engine'
|
|
6
|
+
import { PLAYER, UNIT } from '../common/units'
|
|
7
|
+
import type { ExampleDef } from '../define'
|
|
8
|
+
|
|
9
|
+
export const METEOR_AT = { x: 24, z: 25 }
|
|
10
|
+
|
|
11
|
+
export const meteor: ExampleDef = {
|
|
12
|
+
id: 'custom-fx-meteor',
|
|
13
|
+
category: 'custom-fx',
|
|
14
|
+
title: 'A meteor',
|
|
15
|
+
summary:
|
|
16
|
+
'An effect with a way of its own (`path`): a burning rock falls from the sky trailing fire and light, then the blast, a ring thrown out, rocks flung that trail smoke as they fly (particles that emit particles), the ground cracked and burning. Call one down, or a shower.',
|
|
17
|
+
tags: ['path', 'meteor', 'sub-emitters', 'trail', 'shake', 'decal', 'explosion', 'FxDef'],
|
|
18
|
+
environment: 'dusk',
|
|
19
|
+
start: (world) => {
|
|
20
|
+
for (let i = 0; i < 8; i++) {
|
|
21
|
+
const angle = (i / 8) * Math.PI * 2
|
|
22
|
+
world.spawn(UNIT.footman, { at: { x: METEOR_AT.x + exactMath.cos(angle) * 6, z: METEOR_AT.z + exactMath.sin(angle) * 6 }, owner: PLAYER })
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
source: ['examples/custom-fx/meteor.ts', 'examples/custom-fx/meteor.look.ts']
|
|
26
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An effect from parts, on the client's side: `customFx.blast`, a timeline of parts (`FxPart`) each starting `at`
|
|
3
|
+
* seconds in and lasting `for`, every field a value or a function of the knobs (`params`). Particles burst and stream;
|
|
4
|
+
* a sub-emitter throws sparks off each spark as it dies; a light and a shake grow with `size`; a scorch is laid; another
|
|
5
|
+
* effect (smoke) plays as a part. A ready-made effect is changed by its knobs alone with `fx.extend`
|
|
6
|
+
*/
|
|
7
|
+
import { fx, type FxDef, type FxHandle, type GameClientApi } from '@v12-engine/client'
|
|
8
|
+
import { button, type ExampleLook, select, slider, toggle } from '../../shell/look'
|
|
9
|
+
import { aim } from '../fx/stage'
|
|
10
|
+
import { BLAST_AT } from './parts'
|
|
11
|
+
|
|
12
|
+
/** The blast: everything in it reads `size`, `color` (its heart) and `tint` (its edge) */
|
|
13
|
+
const blast: FxDef = {
|
|
14
|
+
params: { size: 1, color: '#fff0b0', tint: '#ff5a14' },
|
|
15
|
+
knobs: {
|
|
16
|
+
size: { min: 0.3, max: 3, step: 0.1, about: 'How big the blast is' },
|
|
17
|
+
color: { about: 'Its heart' },
|
|
18
|
+
tint: { about: 'Its edge, its sparks' }
|
|
19
|
+
},
|
|
20
|
+
parts: [
|
|
21
|
+
// a white flash: a few big soft dots, gone in a blink
|
|
22
|
+
{
|
|
23
|
+
particles: {
|
|
24
|
+
bursts: [{ at: 0, count: 6 }],
|
|
25
|
+
lifetime: 0.15,
|
|
26
|
+
size: (p) => [4 * Number(p.size), 6 * Number(p.size)],
|
|
27
|
+
color: '#ffffff',
|
|
28
|
+
texture: 'flare'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
// the fireball: puffs flung out and slowing, from the heart's colour to the edge's to smoke
|
|
32
|
+
{
|
|
33
|
+
particles: {
|
|
34
|
+
bursts: [{ at: 0, count: 40 }],
|
|
35
|
+
shape: { type: 'sphere', radius: 0.3 },
|
|
36
|
+
speed: (p) => [2 * Number(p.size), 5 * Number(p.size)],
|
|
37
|
+
drag: 0.9,
|
|
38
|
+
lifetime: [0.5, 0.9],
|
|
39
|
+
size: (p) => [
|
|
40
|
+
[0, 1.2 * Number(p.size)],
|
|
41
|
+
[1, 2.8 * Number(p.size)]
|
|
42
|
+
],
|
|
43
|
+
color: (p) => [
|
|
44
|
+
[0, String(p.color)],
|
|
45
|
+
[0.35, String(p.tint)],
|
|
46
|
+
[1, '#2a201a']
|
|
47
|
+
],
|
|
48
|
+
texture: 'puff',
|
|
49
|
+
rotation: [-1, 1]
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
// sparks thrown high, falling, each throwing a few small ones as it dies (`sub`)
|
|
53
|
+
{
|
|
54
|
+
particles: {
|
|
55
|
+
bursts: [{ at: 0, count: 50 }],
|
|
56
|
+
shape: 'hemisphere',
|
|
57
|
+
spread: 1,
|
|
58
|
+
speed: (p) => [6 * Number(p.size), 12 * Number(p.size)],
|
|
59
|
+
gravity: 9,
|
|
60
|
+
lifetime: [0.6, 1.2],
|
|
61
|
+
size: [0.25, 0.05],
|
|
62
|
+
color: (p) => [String(p.color), String(p.tint)],
|
|
63
|
+
texture: 'spark',
|
|
64
|
+
billboard: 'stretched',
|
|
65
|
+
stretch: 3,
|
|
66
|
+
sub: [
|
|
67
|
+
{
|
|
68
|
+
on: 'death',
|
|
69
|
+
particles: {
|
|
70
|
+
bursts: [{ at: 0, count: 4 }],
|
|
71
|
+
shape: 'sphere',
|
|
72
|
+
speed: [1, 3],
|
|
73
|
+
lifetime: 0.3,
|
|
74
|
+
size: [0.12, 0],
|
|
75
|
+
color: (p) => String(p.tint),
|
|
76
|
+
texture: 'dot'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
// a ring of dust thrown out along the ground
|
|
83
|
+
{
|
|
84
|
+
at: 0.05,
|
|
85
|
+
particles: {
|
|
86
|
+
bursts: [{ at: 0, count: 30 }],
|
|
87
|
+
shape: { type: 'ring', radius: 0.4 },
|
|
88
|
+
speed: (p) => [4 * Number(p.size), 6 * Number(p.size)],
|
|
89
|
+
drag: 0.95,
|
|
90
|
+
lifetime: [0.8, 1.4],
|
|
91
|
+
size: [0.8, 2],
|
|
92
|
+
color: '#9a8466',
|
|
93
|
+
texture: 'puff',
|
|
94
|
+
blend: 'alpha',
|
|
95
|
+
lit: true
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
// the light, as bright as the blast is big, fading; the view shaken; the ground scorched for half a minute
|
|
99
|
+
{ light: { color: (p) => String(p.tint), intensity: (p) => 5 * Number(p.size), range: (p) => 9 * Number(p.size), fade: 0.7 } },
|
|
100
|
+
{ shake: (p) => ({ strength: 0.35 * Number(p.size), time: 0.35 }) },
|
|
101
|
+
{ at: 0.1, decal: { size: (p) => 3.2 * Number(p.size), time: 30, color: '#140d08' } },
|
|
102
|
+
// smoke rising after, another effect of its own played as a part for 5 seconds
|
|
103
|
+
{ at: 0.25, for: 5, fx: 'customFx.smoke', params: (p) => ({ size: p.size }) }
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The smoke after: one emitter, made with `fx.particles` */
|
|
108
|
+
const smoke: FxDef = fx.particles(
|
|
109
|
+
{
|
|
110
|
+
rate: (p) => 18 * Number(p.size),
|
|
111
|
+
over: { rate: [1, 0] },
|
|
112
|
+
shape: { type: 'sphere', radius: 0.6 },
|
|
113
|
+
speed: [0.6, 1.4],
|
|
114
|
+
direction: [0, 1, 0],
|
|
115
|
+
lifetime: [2.5, 4],
|
|
116
|
+
size: (p) => [
|
|
117
|
+
[0, 0.8 * Number(p.size)],
|
|
118
|
+
[1, 3 * Number(p.size)]
|
|
119
|
+
],
|
|
120
|
+
color: ['#4a4440', '#2a2826'],
|
|
121
|
+
alpha: [0.6, 0],
|
|
122
|
+
texture: 'puff',
|
|
123
|
+
blend: 'alpha',
|
|
124
|
+
wind: 1,
|
|
125
|
+
lit: true
|
|
126
|
+
},
|
|
127
|
+
{ params: { size: 1 } }
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
/** The effects of this example, taken into the game's view: the blast, its smoke, and a fire made frost */
|
|
131
|
+
export const partsFx: Record<string, FxDef> = {
|
|
132
|
+
'customFx.blast': blast,
|
|
133
|
+
'customFx.smoke': smoke,
|
|
134
|
+
// the ready-made fire with knobs of its own: blue flames, a cold light, no smoke
|
|
135
|
+
'customFx.frostFire': fx.extend('fire', { color: '#c8f4ff', tint: '#2a7fff', smoke: false, light: 2 })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The colours the card offers, as the blast's heart and edge */
|
|
139
|
+
const COLORS: Record<string, { color: string; tint: string }> = {
|
|
140
|
+
Fire: { color: '#fff0b0', tint: '#ff5a14' },
|
|
141
|
+
Frost: { color: '#f0ffff', tint: '#38a8ff' },
|
|
142
|
+
Venom: { color: '#f4ffb0', tint: '#58d020' },
|
|
143
|
+
Arcane: { color: '#ffe0ff', tint: '#a040ff' }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const chosen = { size: 1, colors: 'Fire' }
|
|
147
|
+
let frostFire: FxHandle | undefined
|
|
148
|
+
|
|
149
|
+
/** Plays the blast where it goes off, with the card's knobs */
|
|
150
|
+
const play = (client: GameClientApi) => client.fx.play('customFx.blast', { at: BLAST_AT, params: { size: chosen.size, ...COLORS[chosen.colors] } })
|
|
151
|
+
|
|
152
|
+
export const partsLook: ExampleLook = {
|
|
153
|
+
controls: [
|
|
154
|
+
button('Blast', (client) => play(client), 'Plays the blast again, at the size and colour chosen'),
|
|
155
|
+
slider(
|
|
156
|
+
'Size',
|
|
157
|
+
{ min: 0.3, max: 3, step: 0.1, value: chosen.size },
|
|
158
|
+
(_client, size) => (chosen.size = size),
|
|
159
|
+
'How big the next blast is, its light and shake too (the knob `size`, read by every part): press Blast to see it'
|
|
160
|
+
),
|
|
161
|
+
select(
|
|
162
|
+
'Colour',
|
|
163
|
+
Object.keys(COLORS),
|
|
164
|
+
chosen.colors,
|
|
165
|
+
(client, name) => {
|
|
166
|
+
chosen.colors = name
|
|
167
|
+
play(client)
|
|
168
|
+
},
|
|
169
|
+
'The blast’s colours, its heart and its edge: choosing plays it'
|
|
170
|
+
),
|
|
171
|
+
toggle(
|
|
172
|
+
'Frost fire',
|
|
173
|
+
true,
|
|
174
|
+
(client, on) => {
|
|
175
|
+
frostFire?.stop({ mode: 'fade', fade: 0.6 })
|
|
176
|
+
frostFire = on ? client.fx.play('customFx.frostFire', { at: { x: BLAST_AT.x - 6, z: BLAST_AT.z + 2 } }) : undefined
|
|
177
|
+
},
|
|
178
|
+
'The blue fire to the left: the ready-made fire made frost by its knobs alone (`fx.extend`)'
|
|
179
|
+
)
|
|
180
|
+
],
|
|
181
|
+
ready: (client) => {
|
|
182
|
+
chosen.size = 1
|
|
183
|
+
chosen.colors = 'Fire'
|
|
184
|
+
aim(client, { x: BLAST_AT.x - 2, z: BLAST_AT.z + 0.5 }, 18, 0.6)
|
|
185
|
+
frostFire = client.fx.play('customFx.frostFire', { at: { x: BLAST_AT.x - 6, z: BLAST_AT.z + 2 } })
|
|
186
|
+
const first = window.setTimeout(() => play(client), 400)
|
|
187
|
+
return () => {
|
|
188
|
+
window.clearTimeout(first)
|
|
189
|
+
frostFire?.stop({ mode: 'at-once' })
|
|
190
|
+
frostFire = undefined
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An effect made of parts, on the rules' side: a knight to stand by it. The effect is the view's (`./parts.look.ts`);
|
|
3
|
+
* the rules could play it too, for every player, with `world.fx('customFx.blast', { at, params })`
|
|
4
|
+
*/
|
|
5
|
+
import { PLAYER, UNIT } from '../common/units'
|
|
6
|
+
import type { ExampleDef } from '../define'
|
|
7
|
+
|
|
8
|
+
export const BLAST_AT = { x: 25.5, z: 25 }
|
|
9
|
+
|
|
10
|
+
export const parts: ExampleDef = {
|
|
11
|
+
id: 'custom-fx-parts',
|
|
12
|
+
category: 'custom-fx',
|
|
13
|
+
title: 'An effect from parts',
|
|
14
|
+
summary:
|
|
15
|
+
'A blast of your own, written as a timeline of parts: a flash, sparks that throw sparks of their own as they die, a fireball, a light, a shake, a scorch, smoke after. Its knobs (size, colour) are sliders here; a ready-made fire is made frost with `fx.extend`.',
|
|
16
|
+
tags: ['FxDef', 'parts', 'knobs', 'params', 'particles', 'sub-emitters', 'fx.extend', 'timeline', 'shake', 'decal'],
|
|
17
|
+
start: (world) => {
|
|
18
|
+
world.spawn(UNIT.knight, { at: { x: BLAST_AT.x - 4, z: BLAST_AT.z - 1 }, owner: PLAYER })
|
|
19
|
+
},
|
|
20
|
+
source: ['examples/custom-fx/parts.ts', 'examples/custom-fx/parts.look.ts']
|
|
21
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two portals, on the client's side. `customFx.gate` is parts: one made in code (`make`: a frame, and a disc drawn by a
|
|
3
|
+
* shader of its own, a swirl turning inward), motes drawn in toward it, and its light. `customFx.warp` is the flash the
|
|
4
|
+
* rules play where a footman leaves and comes: a burst, a ring, a light. The gates are put on the map as the example
|
|
5
|
+
* opens (`MapEnvironment.fx`), the warps played by the rules
|
|
6
|
+
*/
|
|
7
|
+
import { Constants } from '@babylonjs/core/Engines/constants.js'
|
|
8
|
+
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
|
|
9
|
+
import { Color3 } from '@babylonjs/core/Maths/math.color.js'
|
|
10
|
+
import { CreateDisc } from '@babylonjs/core/Meshes/Builders/discBuilder.js'
|
|
11
|
+
import { CreateTorus } from '@babylonjs/core/Meshes/Builders/torusBuilder.js'
|
|
12
|
+
import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
|
|
13
|
+
import type { FxDef, FxInstance, FxContext } from '@v12-engine/client'
|
|
14
|
+
import { button, type ExampleLook } from '../../shell/look'
|
|
15
|
+
import { aim } from '../fx/stage'
|
|
16
|
+
import { PORTALS, stepThrough } from './portal'
|
|
17
|
+
import { type TwoShaders, shaderMaterial } from './shader'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The swirl: in polar terms about the disc's middle, arms that wind tighter toward it and turn with time, between two
|
|
21
|
+
* colours, bright in the middle, fading out at the rim
|
|
22
|
+
*/
|
|
23
|
+
const SWIRL: TwoShaders = {
|
|
24
|
+
glsl: {
|
|
25
|
+
vertex: `precision highp float;
|
|
26
|
+
attribute vec3 position;
|
|
27
|
+
attribute vec2 uv;
|
|
28
|
+
uniform mat4 worldViewProjection;
|
|
29
|
+
varying vec2 vUV;
|
|
30
|
+
void main() {
|
|
31
|
+
gl_Position = worldViewProjection * vec4(position, 1.0);
|
|
32
|
+
vUV = uv;
|
|
33
|
+
}`,
|
|
34
|
+
fragment: `precision highp float;
|
|
35
|
+
uniform float time;
|
|
36
|
+
uniform vec3 inner;
|
|
37
|
+
uniform vec3 outer;
|
|
38
|
+
varying vec2 vUV;
|
|
39
|
+
void main() {
|
|
40
|
+
vec2 p = vUV * 2.0 - 1.0;
|
|
41
|
+
float r = length(p);
|
|
42
|
+
float a = atan(p.y, p.x);
|
|
43
|
+
float arms = sin(a * 5.0 + 6.0 / (r + 0.15) - time * 4.0) * 0.5 + 0.5;
|
|
44
|
+
float fine = sin(a * 11.0 - 9.0 / (r + 0.2) + time * 2.5) * 0.5 + 0.5;
|
|
45
|
+
vec3 color = mix(outer, inner, arms * (1.0 - r) + 0.25) + vec3(1.0) * pow(max(1.0 - r * 1.6, 0.0), 3.0);
|
|
46
|
+
float alpha = (0.35 + arms * 0.5 + fine * 0.25) * smoothstep(1.0, 0.82, r);
|
|
47
|
+
gl_FragColor = vec4(color * alpha, alpha);
|
|
48
|
+
}`
|
|
49
|
+
},
|
|
50
|
+
wgsl: {
|
|
51
|
+
vertex: `attribute position: vec3f;
|
|
52
|
+
attribute uv: vec2f;
|
|
53
|
+
uniform worldViewProjection: mat4x4f;
|
|
54
|
+
varying vUV: vec2f;
|
|
55
|
+
@vertex
|
|
56
|
+
fn main(input: VertexInputs) -> FragmentInputs {
|
|
57
|
+
vertexOutputs.position = uniforms.worldViewProjection * vec4f(vertexInputs.position, 1.0);
|
|
58
|
+
vertexOutputs.vUV = vertexInputs.uv;
|
|
59
|
+
}`,
|
|
60
|
+
fragment: `uniform time: f32;
|
|
61
|
+
uniform inner: vec3f;
|
|
62
|
+
uniform outer: vec3f;
|
|
63
|
+
varying vUV: vec2f;
|
|
64
|
+
@fragment
|
|
65
|
+
fn main(input: FragmentInputs) -> FragmentOutputs {
|
|
66
|
+
let p = fragmentInputs.vUV * 2.0 - 1.0;
|
|
67
|
+
let r = length(p);
|
|
68
|
+
let a = atan2(p.y, p.x);
|
|
69
|
+
let arms = sin(a * 5.0 + 6.0 / (r + 0.15) - uniforms.time * 4.0) * 0.5 + 0.5;
|
|
70
|
+
let fine = sin(a * 11.0 - 9.0 / (r + 0.2) + uniforms.time * 2.5) * 0.5 + 0.5;
|
|
71
|
+
let color = mix(uniforms.outer, uniforms.inner, arms * (1.0 - r) + 0.25) + vec3f(1.0) * pow(max(1.0 - r * 1.6, 0.0), 3.0);
|
|
72
|
+
let alpha = (0.35 + arms * 0.5 + fine * 0.25) * smoothstep(1.0, 0.82, r);
|
|
73
|
+
fragmentOutputs.color = vec4f(color * alpha, alpha);
|
|
74
|
+
}`
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A colour of the knobs as Babylon's */
|
|
79
|
+
const color3 = (hex: unknown) => Color3.FromHexString(String(hex))
|
|
80
|
+
|
|
81
|
+
/** The gate made in code: a stone-and-light frame standing up, the swirl inside it, turning */
|
|
82
|
+
function gate({ scene, anchor, params }: FxContext): FxInstance {
|
|
83
|
+
const root = new TransformNode('customFx.gate', scene)
|
|
84
|
+
root.parent = anchor
|
|
85
|
+
root.position.y = 1.7
|
|
86
|
+
const frame = CreateTorus('customFx.gateFrame', { diameter: 3.2, thickness: 0.28, tessellation: 48 }, scene)
|
|
87
|
+
frame.rotation.x = Math.PI / 2
|
|
88
|
+
frame.parent = root
|
|
89
|
+
const stone = new StandardMaterial('customFx.gateStone', scene)
|
|
90
|
+
stone.diffuseColor = new Color3(0.25, 0.22, 0.28)
|
|
91
|
+
stone.emissiveColor = color3(params.outer).scale(0.6)
|
|
92
|
+
frame.material = stone
|
|
93
|
+
const disc = CreateDisc('customFx.gateSwirl', { radius: 1.5, tessellation: 64 }, scene)
|
|
94
|
+
disc.parent = root
|
|
95
|
+
const swirl = shaderMaterial('customFxSwirl', scene, SWIRL, ['time', 'inner', 'outer'], ['position', 'uv'])
|
|
96
|
+
swirl.alphaMode = Constants.ALPHA_ADD
|
|
97
|
+
swirl.backFaceCulling = false
|
|
98
|
+
disc.material = swirl
|
|
99
|
+
for (const mesh of [frame, disc]) mesh.isPickable = false
|
|
100
|
+
let time = 0
|
|
101
|
+
let inner = color3(params.inner)
|
|
102
|
+
let outer = color3(params.outer)
|
|
103
|
+
return {
|
|
104
|
+
update: (dt) => {
|
|
105
|
+
time += dt
|
|
106
|
+
swirl.setFloat('time', time)
|
|
107
|
+
swirl.setColor3('inner', inner)
|
|
108
|
+
swirl.setColor3('outer', outer)
|
|
109
|
+
},
|
|
110
|
+
set: (next) => {
|
|
111
|
+
inner = color3(next.inner)
|
|
112
|
+
outer = color3(next.outer)
|
|
113
|
+
stone.emissiveColor = outer.scale(0.6)
|
|
114
|
+
},
|
|
115
|
+
dispose: () => {
|
|
116
|
+
root.dispose(false, true)
|
|
117
|
+
swirl.dispose()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The effects of this example, taken into the game's view */
|
|
123
|
+
export const portalFx: Record<string, FxDef> = {
|
|
124
|
+
'customFx.gate': {
|
|
125
|
+
params: { inner: '#e6d4ff', outer: '#7a3cff' },
|
|
126
|
+
parts: [
|
|
127
|
+
{ make: gate },
|
|
128
|
+
// motes drawn in from all round toward the middle of the swirl
|
|
129
|
+
{
|
|
130
|
+
offset: [0, 1.7, 0],
|
|
131
|
+
particles: {
|
|
132
|
+
rate: 50,
|
|
133
|
+
shape: { type: 'sphere', radius: 2.6, rim: true },
|
|
134
|
+
speed: [-2.6, -1.8],
|
|
135
|
+
lifetime: 1,
|
|
136
|
+
size: [0.05, 0.2],
|
|
137
|
+
color: (p) => [String(p.outer), String(p.inner)],
|
|
138
|
+
alpha: [0, 1],
|
|
139
|
+
texture: 'flare'
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{ offset: [0, 1.7, 0.6], light: { color: (p) => String(p.outer), intensity: 2.2, range: 9, flicker: 0.1 } }
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
'customFx.warp': {
|
|
146
|
+
parts: [
|
|
147
|
+
{
|
|
148
|
+
particles: {
|
|
149
|
+
bursts: [{ at: 0, count: 40 }],
|
|
150
|
+
shape: { type: 'cylinder', radius: 0.5, height: 2 },
|
|
151
|
+
direction: [0, 1, 0],
|
|
152
|
+
speed: [2, 6],
|
|
153
|
+
lifetime: [0.4, 0.8],
|
|
154
|
+
size: [0.3, 0],
|
|
155
|
+
color: ['#ffffff', '#b07aff'],
|
|
156
|
+
texture: 'spark',
|
|
157
|
+
billboard: 'stretched',
|
|
158
|
+
stretch: 3
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{ particles: { bursts: [{ at: 0, count: 3 }], lifetime: 0.25, size: [3, 5], color: '#d8c0ff', texture: 'flare', order: 1 }, offset: [0, 1, 0] },
|
|
162
|
+
{ fx: 'shockwave', params: { size: 0.5, color: '#c8a8ff', dust: false } },
|
|
163
|
+
{ light: { color: '#b07aff', intensity: 4, range: 8, fade: 0.5 } }
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const portalLook: ExampleLook = {
|
|
169
|
+
controls: [
|
|
170
|
+
button(
|
|
171
|
+
'Send one through',
|
|
172
|
+
(client) => client.session.send(stepThrough.make({})),
|
|
173
|
+
'A footman steps into one gate and out of the other: a flash at both ends'
|
|
174
|
+
),
|
|
175
|
+
button(
|
|
176
|
+
'Send them all',
|
|
177
|
+
(client) => {
|
|
178
|
+
for (let i = 0; i < 4; i++) window.setTimeout(() => client.session.send(stepThrough.make({})), i * 400)
|
|
179
|
+
},
|
|
180
|
+
'Four footmen through, one after another'
|
|
181
|
+
)
|
|
182
|
+
],
|
|
183
|
+
ready: (client) => {
|
|
184
|
+
// the gates laid on the map as its own effects, the second a warmer one
|
|
185
|
+
client.setEnvironment({
|
|
186
|
+
fx: [
|
|
187
|
+
{ id: 1, name: 'gate 1', fx: 'customFx.gate', ...PORTALS[0] },
|
|
188
|
+
{ id: 2, name: 'gate 2', fx: 'customFx.gate', ...PORTALS[1], params: { inner: '#fff0c8', outer: '#ff7a1a' } }
|
|
189
|
+
]
|
|
190
|
+
})
|
|
191
|
+
aim(client, { x: (PORTALS[0].x + PORTALS[1].x) / 2, z: PORTALS[0].z - 1 }, 25, 0.45)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two portals, on the rules' side: footmen by the first, and a command that sends one through, from whichever portal
|
|
3
|
+
* he is nearer to the other, with a flash at both ends that every player sees (`world.fx`). The portals themselves are
|
|
4
|
+
* the screen's: an effect made in code with a shader of its own (`./portal.look.ts`)
|
|
5
|
+
*/
|
|
6
|
+
import { defineCommand, type Game, exactMath } from '@v12-engine/engine'
|
|
7
|
+
import { PLAYER, UNIT } from '../common/units'
|
|
8
|
+
import type { ExampleDef } from '../define'
|
|
9
|
+
|
|
10
|
+
export const PORTALS = [
|
|
11
|
+
{ x: 18, z: 25 },
|
|
12
|
+
{ x: 30, z: 25 }
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
/** A footman steps through: put down at the other portal, walking on out of it, a flash where he left and where he came */
|
|
16
|
+
export const stepThrough = defineCommand('customFx.step', {}, (world) => {
|
|
17
|
+
const footmen = [...world.units.values()].filter((unit) => unit.def.id === UNIT.footman && unit.alive)
|
|
18
|
+
const away = (unit: { pos: { x: number; z: number } }, at: { x: number; z: number }) => exactMath.hypot(unit.pos.x - at.x, unit.pos.z - at.z)
|
|
19
|
+
// the one nearest either portal
|
|
20
|
+
const one = footmen.sort((a, b) => Math.min(away(a, PORTALS[0]), away(a, PORTALS[1])) - Math.min(away(b, PORTALS[0]), away(b, PORTALS[1])))[0]
|
|
21
|
+
if (!one) return
|
|
22
|
+
const [from, to] = away(one, PORTALS[0]) < away(one, PORTALS[1]) ? PORTALS : [PORTALS[1], PORTALS[0]]
|
|
23
|
+
world.fx('customFx.warp', { at: { x: one.pos.x, z: one.pos.z } })
|
|
24
|
+
world.place(one, { x: to.x, z: to.z - 0.8 })
|
|
25
|
+
world.fx('customFx.warp', { at: { x: to.x, z: to.z - 0.8 } })
|
|
26
|
+
// on out, a few steps the way he was going
|
|
27
|
+
one.order('move', { x: to.x + (to.x - from.x) * 0.25 + (world.rng.next() - 0.5) * 3, z: to.z - 3 - world.rng.next() * 2 })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export const portal: ExampleDef = {
|
|
31
|
+
id: 'custom-fx-portal',
|
|
32
|
+
category: 'custom-fx',
|
|
33
|
+
title: 'Portals (a shader)',
|
|
34
|
+
summary:
|
|
35
|
+
'Two gates, each an effect made in code: a frame, a swirl drawn by a shader of your own, motes drawn in, a light. Send footmen through: the rules move them and play a flash at both ends for every player.',
|
|
36
|
+
tags: ['portal', 'shader', 'ShaderMaterial', 'make', 'teleport', 'world.fx', 'swirl'],
|
|
37
|
+
environment: 'night',
|
|
38
|
+
start: (world) => {
|
|
39
|
+
for (let i = 0; i < 4; i++)
|
|
40
|
+
world.spawn(UNIT.footman, { at: { x: PORTALS[0].x - 1.5 + (i % 2) * 3, z: PORTALS[0].z - 3 - Math.floor(i / 2) * 1.4 }, owner: PLAYER })
|
|
41
|
+
},
|
|
42
|
+
source: ['examples/custom-fx/portal.ts', 'examples/custom-fx/portal.look.ts', 'examples/custom-fx/shader.ts']
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Registers the command */
|
|
46
|
+
export function portalRules(game: Game<any>): void {
|
|
47
|
+
game.command(stepThrough)
|
|
48
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A renderer of your own, on the client's side: a `RendererFactory` in the view's `renderers`, handed the render kit
|
|
3
|
+
* (`RenderKit`: the scene, the state the screen knows, where each unit is drawn). It gives back a `FeatureRenderer`:
|
|
4
|
+
* `sync` hears what each snapshot brought (units come and gone, events: an attack, a blow), `frame` draws every
|
|
5
|
+
* frame, `dispose` takes it all away. Here, a ring under every unit coloured by its health, flaring at a blow, and a
|
|
6
|
+
* line from each attacker to what it attacks
|
|
7
|
+
*/
|
|
8
|
+
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
|
|
9
|
+
import { Color3 } from '@babylonjs/core/Maths/math.color.js'
|
|
10
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector.js'
|
|
11
|
+
import { CreateLines } from '@babylonjs/core/Meshes/Builders/linesBuilder.js'
|
|
12
|
+
import { CreateTorus } from '@babylonjs/core/Meshes/Builders/torusBuilder.js'
|
|
13
|
+
import type { LinesMesh } from '@babylonjs/core/Meshes/linesMesh.js'
|
|
14
|
+
import type { Mesh } from '@babylonjs/core/Meshes/mesh.js'
|
|
15
|
+
import type { RendererFactory } from '@v12-engine/client'
|
|
16
|
+
import { button, type ExampleLook, toggle } from '../../shell/look'
|
|
17
|
+
import { ENEMY } from '../common/units'
|
|
18
|
+
import { clash, lineUpAgain } from '../fx/blood'
|
|
19
|
+
import { aim } from '../fx/stage'
|
|
20
|
+
|
|
21
|
+
/** Whether the card shows what the renderer draws */
|
|
22
|
+
const shown = { rings: true, lines: true }
|
|
23
|
+
|
|
24
|
+
const GREEN = new Color3(0.2, 1, 0.35)
|
|
25
|
+
const YELLOW = new Color3(1, 0.85, 0.2)
|
|
26
|
+
const RED = new Color3(1, 0.15, 0.1)
|
|
27
|
+
|
|
28
|
+
/** Green when whole, yellow at half, red near none */
|
|
29
|
+
const healthColor = (share: number, out: Color3) =>
|
|
30
|
+
share > 0.5 ? Color3.LerpToRef(YELLOW, GREEN, (share - 0.5) * 2, out) : Color3.LerpToRef(RED, YELLOW, share * 2, out)
|
|
31
|
+
|
|
32
|
+
const ringsAndLines: RendererFactory = (kit) => {
|
|
33
|
+
const rings = new Map<number, { mesh: Mesh; material: StandardMaterial; flare: number }>()
|
|
34
|
+
const lines: { mesh: LinesMesh; from: number; to: number; age: number }[] = []
|
|
35
|
+
const ringOf = (id: number) => {
|
|
36
|
+
let ring = rings.get(id)
|
|
37
|
+
if (ring) return ring
|
|
38
|
+
const mesh = CreateTorus('customFx.healthRing', { diameter: 1.25, thickness: 0.07, tessellation: 32 }, kit.scene)
|
|
39
|
+
const material = new StandardMaterial('customFx.healthRing', kit.scene)
|
|
40
|
+
material.disableLighting = true
|
|
41
|
+
mesh.material = material
|
|
42
|
+
mesh.isPickable = false
|
|
43
|
+
ring = { mesh, material, flare: 0 }
|
|
44
|
+
rings.set(id, ring)
|
|
45
|
+
return ring
|
|
46
|
+
}
|
|
47
|
+
/** Where a unit's line ends: over its head, so the line reads above the crowd */
|
|
48
|
+
const middle = (id: number) => {
|
|
49
|
+
const drawn = kit.drawn(id)
|
|
50
|
+
return drawn && drawn.visible ? drawn.position.add(new Vector3(0, 2.3, 0)) : undefined
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
sync(changes) {
|
|
54
|
+
for (const event of changes.events) {
|
|
55
|
+
// a blow: the ring of the struck flares
|
|
56
|
+
if (event.type === 'damage') ringOf(event.id).flare = 1
|
|
57
|
+
// an attack begun: a line from the attacker to its target, its side's colour
|
|
58
|
+
if (event.type === 'attack' && event.target !== undefined) {
|
|
59
|
+
const a = middle(event.id)
|
|
60
|
+
const b = middle(event.target)
|
|
61
|
+
if (!a || !b) continue
|
|
62
|
+
const mesh = CreateLines('customFx.threat', { points: [a, b], updatable: true }, kit.scene)
|
|
63
|
+
mesh.color = kit.state.entities.get(event.id)?.owner === ENEMY ? new Color3(1, 0.35, 0.3) : new Color3(0.4, 0.8, 1)
|
|
64
|
+
mesh.isPickable = false
|
|
65
|
+
lines.push({ mesh, from: event.id, to: event.target, age: 0 })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const gone of changes.removed) {
|
|
69
|
+
const ring = rings.get(gone.id)
|
|
70
|
+
ring?.mesh.dispose()
|
|
71
|
+
ring?.material.dispose()
|
|
72
|
+
rings.delete(gone.id)
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
frame(_alpha, dt) {
|
|
76
|
+
for (const entity of kit.state.entities.values()) {
|
|
77
|
+
const drawn = kit.drawn(entity.id)
|
|
78
|
+
const ring = ringOf(entity.id)
|
|
79
|
+
const share = entity.health / Math.max(entity.maxHealth, 1)
|
|
80
|
+
ring.mesh.setEnabled(shown.rings && !!drawn?.visible && entity.health > 0)
|
|
81
|
+
if (!drawn) continue
|
|
82
|
+
ring.mesh.position.set(drawn.position.x, drawn.position.y + 0.05, drawn.position.z)
|
|
83
|
+
// a blow flares it white and wide, fading back in a third of a second
|
|
84
|
+
ring.flare = Math.max(ring.flare - dt * 3, 0)
|
|
85
|
+
healthColor(share, ring.material.emissiveColor)
|
|
86
|
+
Color3.LerpToRef(ring.material.emissiveColor, Color3.White(), ring.flare, ring.material.emissiveColor)
|
|
87
|
+
ring.mesh.scaling.setAll(1 + ring.flare * 0.35)
|
|
88
|
+
}
|
|
89
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
90
|
+
const line = lines[i]!
|
|
91
|
+
line.age += dt
|
|
92
|
+
const a = middle(line.from)
|
|
93
|
+
const b = middle(line.to)
|
|
94
|
+
if (line.age > 0.8 || !a || !b) {
|
|
95
|
+
line.mesh.dispose()
|
|
96
|
+
lines.splice(i, 1)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
CreateLines('customFx.threat', { points: [a, b], instance: line.mesh })
|
|
100
|
+
line.mesh.alpha = 1 - line.age / 0.8
|
|
101
|
+
line.mesh.setEnabled(shown.lines)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
dispose() {
|
|
105
|
+
for (const ring of rings.values()) {
|
|
106
|
+
ring.mesh.dispose()
|
|
107
|
+
ring.material.dispose()
|
|
108
|
+
}
|
|
109
|
+
for (const line of lines) line.mesh.dispose()
|
|
110
|
+
rings.clear()
|
|
111
|
+
lines.length = 0
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const rendererLook: ExampleLook = {
|
|
117
|
+
view: (view) => ({ ...view, renderers: [...(view.renderers ?? []), ringsAndLines] }),
|
|
118
|
+
controls: [
|
|
119
|
+
button(
|
|
120
|
+
'Fight',
|
|
121
|
+
(client) => client.session.send(clash.make({})),
|
|
122
|
+
'Both sides go at each other: the rings redden as they are hurt, lines join each attacker to its target'
|
|
123
|
+
),
|
|
124
|
+
button('Line them up again', (client) => client.session.send(lineUpAgain.make({})), 'Everyone back in line, whole, their rings green again'),
|
|
125
|
+
toggle(
|
|
126
|
+
'Health rings',
|
|
127
|
+
true,
|
|
128
|
+
(_client, on) => (shown.rings = on),
|
|
129
|
+
'The ring under each unit, green to red as it is hurt, flaring white at a blow'
|
|
130
|
+
),
|
|
131
|
+
toggle('Attack lines', true, (_client, on) => (shown.lines = on), 'A fading line from each attacker to what it attacks, as it strikes')
|
|
132
|
+
],
|
|
133
|
+
ready: (client) => {
|
|
134
|
+
shown.rings = shown.lines = true
|
|
135
|
+
aim(client, { x: 24, z: 24 }, 18, 0.8)
|
|
136
|
+
const go = window.setTimeout(() => client.session.send(clash.make({})), 1200)
|
|
137
|
+
return () => window.clearTimeout(go)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A renderer of your own, on the rules' side: the footmen and skeletons of the blood example's fight, on a field of
|
|
3
|
+
* their own. What is drawn over them is the screen's (`./renderer.look.ts`)
|
|
4
|
+
*/
|
|
5
|
+
import { type ExampleDef, exampleField } from '../define'
|
|
6
|
+
import { blood } from '../fx/blood'
|
|
7
|
+
|
|
8
|
+
export const renderer: ExampleDef = {
|
|
9
|
+
id: 'custom-fx-renderer',
|
|
10
|
+
category: 'custom-fx',
|
|
11
|
+
title: 'A renderer of your own',
|
|
12
|
+
summary:
|
|
13
|
+
"Something drawn for every unit by code of the game's own (`GameView.renderers`): a ring under each, from green to red as it is hurt, flaring white at each blow; a line from each attacker to what it attacks, fading. Fight, and watch the field read itself.",
|
|
14
|
+
tags: ['renderer', 'RendererFactory', 'FeatureRenderer', 'sync', 'events', 'health', 'lines', 'kit'],
|
|
15
|
+
map: () => exampleField({ name: 'Field', players: 2 }),
|
|
16
|
+
start: blood.start,
|
|
17
|
+
source: ['examples/custom-fx/renderer.ts', 'examples/custom-fx/renderer.look.ts', 'examples/fx/blood.ts']
|
|
18
|
+
}
|