@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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passes of our own over the finished picture: each a fragment shader, made into a `PostProcess` and put on with
|
|
3
|
+
* `client.picture.addPass`, which keeps it in its place in the camera's chain whatever else comes and goes. A pass reads
|
|
4
|
+
* the picture as `textureSampler` at `vUV` and writes the colour shown. Each is written twice, as the client's own are:
|
|
5
|
+
* in WGSL for WebGPU and in GLSL for WebGL (Babylon does not turn one into the other in the page)
|
|
6
|
+
*/
|
|
7
|
+
import { ShaderLanguage } from '@babylonjs/core/Materials/shaderLanguage.js'
|
|
8
|
+
import { ShaderStore } from '@babylonjs/core/Engines/shaderStore.js'
|
|
9
|
+
import { PostProcess } from '@babylonjs/core/PostProcesses/postProcess.js'
|
|
10
|
+
import type { GameClientApi, PicturePass } from '@v12-engine/client'
|
|
11
|
+
|
|
12
|
+
/** What every pass is given in GLSL: the picture, where on it, its size in pixels, the time, and a strength of 0 to 1 */
|
|
13
|
+
const GLSL_HEAD = /* glsl */ `
|
|
14
|
+
precision highp float;
|
|
15
|
+
varying vec2 vUV;
|
|
16
|
+
uniform sampler2D textureSampler;
|
|
17
|
+
uniform vec2 screen;
|
|
18
|
+
uniform float time;
|
|
19
|
+
uniform float amount;
|
|
20
|
+
vec3 picture(vec2 uv) { return texture2D(textureSampler, uv).rgb; }
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
/** The same in WGSL: the uniforms are read as `uniforms.*`, the picture sampled the same way everywhere (`picture`) */
|
|
24
|
+
const WGSL_HEAD = /* wgsl */ `
|
|
25
|
+
varying vUV: vec2f;
|
|
26
|
+
var textureSamplerSampler: sampler;
|
|
27
|
+
var textureSampler: texture_2d<f32>;
|
|
28
|
+
uniform screen: vec2f;
|
|
29
|
+
uniform time: f32;
|
|
30
|
+
uniform amount: f32;
|
|
31
|
+
fn picture(uv: vec2f) -> vec3f { return textureSampleLevel(textureSampler, textureSamplerSampler, uv, 0.0).rgb; }
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
/** A pass in both languages: the body after the head, its colour `gl_FragColor` in GLSL and `fragmentOutputs.color` in WGSL */
|
|
35
|
+
interface Shader {
|
|
36
|
+
glsl: string
|
|
37
|
+
wgsl: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The passes, by name */
|
|
41
|
+
export const SHADERS = {
|
|
42
|
+
// big square pixels and few colours: an old console's
|
|
43
|
+
Pixels: {
|
|
44
|
+
glsl: `
|
|
45
|
+
void main(void) {
|
|
46
|
+
float size = mix(2.0, 9.0, amount);
|
|
47
|
+
vec2 cell = floor(vUV * screen / size) * size + size * 0.5;
|
|
48
|
+
float levels = mix(24.0, 5.0, amount);
|
|
49
|
+
gl_FragColor = vec4(floor(picture(cell / screen) * levels + 0.5) / levels, 1.0);
|
|
50
|
+
}`,
|
|
51
|
+
wgsl: `
|
|
52
|
+
@fragment
|
|
53
|
+
fn main(input: FragmentInputs) -> FragmentOutputs {
|
|
54
|
+
let size = mix(2.0, 9.0, uniforms.amount);
|
|
55
|
+
let cell = floor(input.vUV * uniforms.screen / size) * size + size * 0.5;
|
|
56
|
+
let levels = mix(24.0, 5.0, uniforms.amount);
|
|
57
|
+
fragmentOutputs.color = vec4f(floor(picture(cell / uniforms.screen) * levels + 0.5) / levels, 1.0);
|
|
58
|
+
}`
|
|
59
|
+
},
|
|
60
|
+
// a curved tube's screen: lines across, the picture bent at its edges, red, green and blue in stripes
|
|
61
|
+
CRT: {
|
|
62
|
+
glsl: `
|
|
63
|
+
void main(void) {
|
|
64
|
+
vec2 centred = vUV * 2.0 - 1.0;
|
|
65
|
+
vec2 uv = centred * (1.0 + dot(centred.yx, centred.yx) * 0.08 * amount) * 0.5 + 0.5;
|
|
66
|
+
float inside = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0);
|
|
67
|
+
vec3 color = picture(clamp(uv, 0.0, 1.0));
|
|
68
|
+
float line = 0.75 + 0.25 * sin(uv.y * screen.y * 3.14159);
|
|
69
|
+
float column = mod(floor(uv.x * screen.x), 3.0);
|
|
70
|
+
vec3 mask = column < 1.0 ? vec3(1.0, 0.75, 0.75) : column < 2.0 ? vec3(0.75, 1.0, 0.75) : vec3(0.75, 0.75, 1.0);
|
|
71
|
+
color *= mix(vec3(1.0), mask * line * 1.25, amount);
|
|
72
|
+
color *= 1.0 - dot(centred, centred) * 0.25 * amount;
|
|
73
|
+
gl_FragColor = vec4(color * inside, 1.0);
|
|
74
|
+
}`,
|
|
75
|
+
wgsl: `
|
|
76
|
+
@fragment
|
|
77
|
+
fn main(input: FragmentInputs) -> FragmentOutputs {
|
|
78
|
+
let centred = input.vUV * 2.0 - 1.0;
|
|
79
|
+
let uv = centred * (1.0 + dot(centred.yx, centred.yx) * 0.08 * uniforms.amount) * 0.5 + 0.5;
|
|
80
|
+
let inside = step(0.0, uv.x) * step(uv.x, 1.0) * step(0.0, uv.y) * step(uv.y, 1.0);
|
|
81
|
+
var color = picture(clamp(uv, vec2f(0.0), vec2f(1.0)));
|
|
82
|
+
let line = 0.75 + 0.25 * sin(uv.y * uniforms.screen.y * 3.14159);
|
|
83
|
+
let column = floor(uv.x * uniforms.screen.x) % 3.0;
|
|
84
|
+
var mask = vec3f(0.75, 0.75, 1.0);
|
|
85
|
+
if (column < 1.0) { mask = vec3f(1.0, 0.75, 0.75); } else if (column < 2.0) { mask = vec3f(0.75, 1.0, 0.75); }
|
|
86
|
+
color = color * mix(vec3f(1.0), mask * line * 1.25, uniforms.amount);
|
|
87
|
+
color = color * (1.0 - dot(centred, centred) * 0.25 * uniforms.amount);
|
|
88
|
+
fragmentOutputs.color = vec4f(color * inside, 1.0);
|
|
89
|
+
}`
|
|
90
|
+
},
|
|
91
|
+
// under water: the picture wavers, green-blue, light dancing over it
|
|
92
|
+
'Under water': {
|
|
93
|
+
glsl: `
|
|
94
|
+
void main(void) {
|
|
95
|
+
vec2 uv = vUV + vec2(sin(vUV.y * 24.0 + time * 1.7), cos(vUV.x * 20.0 + time * 1.3)) * 0.004 * amount;
|
|
96
|
+
vec3 color = picture(uv);
|
|
97
|
+
float caustic = pow(abs(sin(uv.x * 40.0 + time) * sin(uv.y * 34.0 - time * 1.2)), 6.0);
|
|
98
|
+
vec3 water = color * vec3(0.45, 0.8, 0.95) + vec3(0.0, 0.05, 0.1) + caustic * 0.25;
|
|
99
|
+
gl_FragColor = vec4(mix(color, water, amount), 1.0);
|
|
100
|
+
}`,
|
|
101
|
+
wgsl: `
|
|
102
|
+
@fragment
|
|
103
|
+
fn main(input: FragmentInputs) -> FragmentOutputs {
|
|
104
|
+
let t = uniforms.time;
|
|
105
|
+
let uv = input.vUV + vec2f(sin(input.vUV.y * 24.0 + t * 1.7), cos(input.vUV.x * 20.0 + t * 1.3)) * 0.004 * uniforms.amount;
|
|
106
|
+
let color = picture(uv);
|
|
107
|
+
let caustic = pow(abs(sin(uv.x * 40.0 + t) * sin(uv.y * 34.0 - t * 1.2)), 6.0);
|
|
108
|
+
let water = color * vec3f(0.45, 0.8, 0.95) + vec3f(0.0, 0.05, 0.1) + caustic * 0.25;
|
|
109
|
+
fragmentOutputs.color = vec4f(mix(color, water, uniforms.amount), 1.0);
|
|
110
|
+
}`
|
|
111
|
+
},
|
|
112
|
+
// a pencil drawing: the edges of what is drawn in grey lines on paper, the darks hatched
|
|
113
|
+
Sketch: {
|
|
114
|
+
glsl: `
|
|
115
|
+
float lum(vec2 uv) { return dot(picture(uv), vec3(0.299, 0.587, 0.114)); }
|
|
116
|
+
void main(void) {
|
|
117
|
+
vec2 p = 1.0 / screen;
|
|
118
|
+
float gx = lum(vUV + vec2(p.x, 0.0)) - lum(vUV - vec2(p.x, 0.0));
|
|
119
|
+
float gy = lum(vUV + vec2(0.0, p.y)) - lum(vUV - vec2(0.0, p.y));
|
|
120
|
+
float edge = clamp(length(vec2(gx, gy)) * 6.0, 0.0, 1.0);
|
|
121
|
+
float hatch = step(0.5, fract((vUV.x + vUV.y) * screen.y * 0.18)) * step(lum(vUV), 0.35) * 0.25;
|
|
122
|
+
vec3 paper = vec3(0.96, 0.94, 0.88) * (1.0 - edge * 0.85 - hatch);
|
|
123
|
+
gl_FragColor = vec4(mix(picture(vUV), paper, amount), 1.0);
|
|
124
|
+
}`,
|
|
125
|
+
wgsl: `
|
|
126
|
+
fn lum(uv: vec2f) -> f32 { return dot(picture(uv), vec3f(0.299, 0.587, 0.114)); }
|
|
127
|
+
@fragment
|
|
128
|
+
fn main(input: FragmentInputs) -> FragmentOutputs {
|
|
129
|
+
let uv = input.vUV;
|
|
130
|
+
let p = 1.0 / uniforms.screen;
|
|
131
|
+
let gx = lum(uv + vec2f(p.x, 0.0)) - lum(uv - vec2f(p.x, 0.0));
|
|
132
|
+
let gy = lum(uv + vec2f(0.0, p.y)) - lum(uv - vec2f(0.0, p.y));
|
|
133
|
+
let edge = clamp(length(vec2f(gx, gy)) * 6.0, 0.0, 1.0);
|
|
134
|
+
let hatch = step(0.5, fract((uv.x + uv.y) * uniforms.screen.y * 0.18)) * step(lum(uv), 0.35) * 0.25;
|
|
135
|
+
let paper = vec3f(0.96, 0.94, 0.88) * (1.0 - edge * 0.85 - hatch);
|
|
136
|
+
fragmentOutputs.color = vec4f(mix(picture(uv), paper, uniforms.amount), 1.0);
|
|
137
|
+
}`
|
|
138
|
+
},
|
|
139
|
+
// a heat camera's: how bright as a colour, cold blue to white hot
|
|
140
|
+
'Heat vision': {
|
|
141
|
+
glsl: `
|
|
142
|
+
vec3 heat(float t) {
|
|
143
|
+
return clamp(vec3(1.5 * t - 0.25, 1.8 * t * t - 0.4, 0.8 - 1.6 * abs(t - 0.3)) + vec3(0.0, 0.0, 0.2) * (1.0 - t), 0.0, 1.0);
|
|
144
|
+
}
|
|
145
|
+
void main(void) {
|
|
146
|
+
vec3 color = picture(vUV);
|
|
147
|
+
gl_FragColor = vec4(mix(color, heat(dot(color, vec3(0.299, 0.587, 0.114))), amount), 1.0);
|
|
148
|
+
}`,
|
|
149
|
+
wgsl: `
|
|
150
|
+
fn heat(t: f32) -> vec3f {
|
|
151
|
+
return clamp(vec3f(1.5 * t - 0.25, 1.8 * t * t - 0.4, 0.8 - 1.6 * abs(t - 0.3)) + vec3f(0.0, 0.0, 0.2) * (1.0 - t), vec3f(0.0), vec3f(1.0));
|
|
152
|
+
}
|
|
153
|
+
@fragment
|
|
154
|
+
fn main(input: FragmentInputs) -> FragmentOutputs {
|
|
155
|
+
let color = picture(input.vUV);
|
|
156
|
+
fragmentOutputs.color = vec4f(mix(color, heat(dot(color, vec3f(0.299, 0.587, 0.114))), uniforms.amount), 1.0);
|
|
157
|
+
}`
|
|
158
|
+
}
|
|
159
|
+
} satisfies Record<string, Shader>
|
|
160
|
+
|
|
161
|
+
/** A pass's name */
|
|
162
|
+
export type ShaderName = keyof typeof SHADERS
|
|
163
|
+
|
|
164
|
+
/** A pass of our own put on, and how strongly it is drawn (0 to 1) */
|
|
165
|
+
export interface OwnPass {
|
|
166
|
+
pass: PicturePass
|
|
167
|
+
amount: number
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Puts a pass of ours on the picture, after it is finished (`before: 'screen'`, the default): its shader stored once
|
|
172
|
+
* under its name in the language the engine draws with, its strength read every frame from what is given back.
|
|
173
|
+
* `remove()` on its `pass` takes it off
|
|
174
|
+
*/
|
|
175
|
+
export function addShader(client: GameClientApi, name: ShaderName, amount = 1): OwnPass {
|
|
176
|
+
const id = `examplesPost${name.replace(/[^a-z]/gi, '')}`
|
|
177
|
+
ShaderStore.ShadersStore[`${id}PixelShader`] ??= GLSL_HEAD + SHADERS[name].glsl
|
|
178
|
+
ShaderStore.ShadersStoreWGSL[`${id}PixelShader`] ??= WGSL_HEAD + SHADERS[name].wgsl
|
|
179
|
+
const started = performance.now()
|
|
180
|
+
const own: OwnPass = { amount, pass: undefined as unknown as PicturePass }
|
|
181
|
+
own.pass = client.picture.addPass(
|
|
182
|
+
({ engine }) => {
|
|
183
|
+
const language = engine.isWebGPU ? ShaderLanguage.WGSL : ShaderLanguage.GLSL
|
|
184
|
+
// (name, shader, uniforms, samplers, ratio, camera, sampling, engine, reusable, defines, texture type, vertex, index uniforms, block compilation, format, language)
|
|
185
|
+
const pass = new PostProcess(
|
|
186
|
+
name,
|
|
187
|
+
id,
|
|
188
|
+
['screen', 'time', 'amount'],
|
|
189
|
+
null,
|
|
190
|
+
1,
|
|
191
|
+
null,
|
|
192
|
+
undefined,
|
|
193
|
+
engine,
|
|
194
|
+
false,
|
|
195
|
+
undefined,
|
|
196
|
+
undefined,
|
|
197
|
+
undefined,
|
|
198
|
+
undefined,
|
|
199
|
+
undefined,
|
|
200
|
+
undefined,
|
|
201
|
+
language
|
|
202
|
+
)
|
|
203
|
+
pass.onApply = (effect) => {
|
|
204
|
+
effect.setFloat2('screen', pass.width, pass.height)
|
|
205
|
+
effect.setFloat('time', (performance.now() - started) / 1000)
|
|
206
|
+
effect.setFloat('amount', own.amount)
|
|
207
|
+
}
|
|
208
|
+
return pass
|
|
209
|
+
},
|
|
210
|
+
{ name }
|
|
211
|
+
)
|
|
212
|
+
return own
|
|
213
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A computer opponent: the engine's melee brain seated at the red player (`world.computers.seat`). It sees only what
|
|
3
|
+
* its side sees, and plays as a player does: it puts its peasants to work, builds farms and a barracks, trains an army
|
|
4
|
+
* and, once it has a wave, comes for your base. Yours has a barracks, a tower and a few footmen to meet it
|
|
5
|
+
*/
|
|
6
|
+
import { defineCommand, type World } from '@v12-engine/engine'
|
|
7
|
+
import { meleeBrain } from '@v12-engine/engine/ai'
|
|
8
|
+
import { ENEMY, PLAYER } from '../common/units'
|
|
9
|
+
import { type ExampleDef, exampleField } from '../define'
|
|
10
|
+
import { base, clear, purse, startWork } from './base'
|
|
11
|
+
import { RTS } from './data'
|
|
12
|
+
|
|
13
|
+
/** The two bases, at opposite corners of the field */
|
|
14
|
+
export const HOME = { x: 18, z: 16 }
|
|
15
|
+
export const THEIRS = { x: 50, z: 48 }
|
|
16
|
+
|
|
17
|
+
/** Both bases, their peasants at work; yours with a barracks, a farm, a tower and four footmen; the computer seated at theirs */
|
|
18
|
+
function layOut(world: World): void {
|
|
19
|
+
clear(world)
|
|
20
|
+
purse(world, PLAYER, 600, 300)
|
|
21
|
+
purse(world, ENEMY, 800, 400)
|
|
22
|
+
base(world, PLAYER, HOME)
|
|
23
|
+
startWork(world, PLAYER, 3)
|
|
24
|
+
world.spawn(RTS.barracks, { at: { x: 24.5, z: 22.5 }, owner: PLAYER })
|
|
25
|
+
world.spawn(RTS.farm, { at: { x: 15, z: 10 }, owner: PLAYER })
|
|
26
|
+
world.spawn(RTS.tower, { at: { x: 26, z: 27 }, owner: PLAYER })
|
|
27
|
+
for (let i = 0; i < 4; i++) world.spawn(RTS.footman, { at: { x: 22 + i * 1.4, z: 26 }, owner: PLAYER, facing: 0.8 })
|
|
28
|
+
base(world, ENEMY, THEIRS)
|
|
29
|
+
startWork(world, ENEMY, 3)
|
|
30
|
+
// the engine's melee brain, handed over itself: its first wave of four after a minute and a half, then every minute
|
|
31
|
+
world.computers.seat(ENEMY, { brain: meleeBrain, enemyBases: [HOME], settings: { army: { firstAttack: 90, waveSize: 4, attackEvery: 60 } } })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Tells the computer to attack now, with what it has (a mission's script talks to its brain the same way) */
|
|
35
|
+
export const attackNow = defineCommand('rts.attackNow', {}, (world) => void world.computers.tell(ENEMY, 'attack', { at: HOME }))
|
|
36
|
+
|
|
37
|
+
export const resetAi = defineCommand('rts.resetAi', {}, (world) => layOut(world))
|
|
38
|
+
|
|
39
|
+
export const ai: ExampleDef = {
|
|
40
|
+
id: 'rts-ai',
|
|
41
|
+
category: 'rts',
|
|
42
|
+
title: 'A computer opponent',
|
|
43
|
+
summary:
|
|
44
|
+
'The red player is the computer: the engine’s melee brain gathers, builds, trains and sends waves at your base, seeing only what its side sees. Defend with what you have, train more, or look in on its base.',
|
|
45
|
+
tags: ['ai', 'computer', 'opponent', 'brain', 'bot', 'enemy', 'waves', 'skirmish', 'melee'],
|
|
46
|
+
map: () => exampleField({ name: 'Skirmish', size: 64, players: 2 }),
|
|
47
|
+
start: (world) => layOut(world),
|
|
48
|
+
source: ['examples/rts/ai.ts', 'examples/rts/base.ts', 'examples/rts/data.ts', 'examples/rts/looks.ts']
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A zeppelin over a cliff wall: a squad below the wall cannot walk up it (it has no ramp), so they board a zeppelin,
|
|
3
|
+
* which flies up over the cliff and lets them out on the plateau. A flying transport is a unit type with `flying` and
|
|
4
|
+
* `transport`: its passengers walk under it to get in, and get out on the ground nearest it
|
|
5
|
+
*/
|
|
6
|
+
import { defineCommand, type World } from '@v12-engine/engine'
|
|
7
|
+
import { PLAYER, UNIT } from '../common/units'
|
|
8
|
+
import type { ExampleDef } from '../define'
|
|
9
|
+
import { clear } from './base'
|
|
10
|
+
import { CARRY, route } from './carry'
|
|
11
|
+
import { RTS } from './data'
|
|
12
|
+
import { edgeAt, plateauMap } from './waterside'
|
|
13
|
+
|
|
14
|
+
/** Where the zeppelin waits over the squad, where it flies to over the plateau, and where it lets them out up there */
|
|
15
|
+
const MOOR = { x: 15, z: 20 }
|
|
16
|
+
const ACROSS = { x: edgeAt(20) + 7, z: 20 }
|
|
17
|
+
const LAND = { x: edgeAt(20) + 8, z: 20 }
|
|
18
|
+
|
|
19
|
+
/** The squad below the wall, the zeppelin hanging over them, and a gold mine up on the plateau out of their reach */
|
|
20
|
+
export function layOutAirlift(world: World): void {
|
|
21
|
+
clear(world)
|
|
22
|
+
world.setState(route, { moor: MOOR, across: ACROSS, land: LAND })
|
|
23
|
+
world.spawn(CARRY.zeppelin, { at: MOOR, owner: PLAYER, facing: Math.PI / 2 })
|
|
24
|
+
const squad = [UNIT.footman, UNIT.footman, UNIT.footman, UNIT.archer, UNIT.archer, UNIT.mage]
|
|
25
|
+
squad.forEach((type, i) =>
|
|
26
|
+
world.spawn(type, { at: { x: 9.5 + Math.floor(i / 3) * 1.8, z: 18.2 + (i % 3) * 1.8 }, owner: PLAYER, facing: Math.PI / 2 })
|
|
27
|
+
)
|
|
28
|
+
world.spawn(RTS.goldmine, { at: { x: edgeAt(20) + 13, z: 26 }, owner: 0 })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const resetAirlift = defineCommand('rts.resetAirlift', {}, (world) => layOutAirlift(world))
|
|
32
|
+
|
|
33
|
+
export const airlift: ExampleDef = {
|
|
34
|
+
id: 'rts-zeppelin',
|
|
35
|
+
category: 'rts',
|
|
36
|
+
title: 'A zeppelin over a cliff',
|
|
37
|
+
summary:
|
|
38
|
+
'Nothing on foot gets up the cliff wall. Board the squad onto the zeppelin, fly it over the wall and unload on the plateau, or by hand: right-click the zeppelin with your units, then select it and right-click up top.',
|
|
39
|
+
tags: ['transport', 'zeppelin', 'airship', 'flying', 'air', 'board', 'load', 'unload', 'cargo', 'cliff', 'plateau', 'drop'],
|
|
40
|
+
map: plateauMap,
|
|
41
|
+
start: (world) => layOutAirlift(world),
|
|
42
|
+
source: ['examples/rts/airlift.ts', 'examples/rts/carry.ts', 'examples/rts/waterside.ts', 'examples/rts/looks.ts']
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the RTS examples share on the rules' side: a base laid out (a hall, its gold mine, a wood behind it, workers),
|
|
3
|
+
* a player's purse set, and commands the cards send to train, build and gather as a player's clicks would
|
|
4
|
+
*/
|
|
5
|
+
import { defineCommand, defineTimer, type Entity, string, type Vec2, type World, exactMath } from '@v12-engine/engine'
|
|
6
|
+
import { RTS } from './data'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A player's live entities, of a type or all. Of every entity (`world.entities`): what is no player's and only
|
|
10
|
+
* stands there (a mine, a tree, a shop) is scenery, kept out of `world.units`, the entities that act
|
|
11
|
+
*/
|
|
12
|
+
export const ownedOf = (world: World, player: number, type?: string): Entity[] =>
|
|
13
|
+
[...world.entities.values()].filter((each) => each.alive && each.owner === player && (type === undefined || each.def.id === type))
|
|
14
|
+
|
|
15
|
+
/** Takes everything away but the trees: what an example laid out again starts from */
|
|
16
|
+
export function clear(world: World): void {
|
|
17
|
+
for (const entity of [...world.entities.values()]) if (entity.alive && entity.def.id !== RTS.tree) world.kill(entity, undefined, false, 'removed')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Sets what a player has of each resource */
|
|
21
|
+
export function purse(world: World, player: number, gold: number, lumber: number): void {
|
|
22
|
+
const resources = world.player(player).resources
|
|
23
|
+
resources.gold = gold
|
|
24
|
+
resources.lumber = lumber
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Where a base's parts stand round its hall: the mine to the west, the wood to the north */
|
|
28
|
+
export interface Base {
|
|
29
|
+
hall: Entity
|
|
30
|
+
mine: Entity
|
|
31
|
+
workers: Entity[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A base at `at`: a town hall (or a keep), a gold mine 9 cells west of it, a wood of trees north of the two, and
|
|
36
|
+
* `workers` peasants by the hall, of `player`'s. The trees and the mine are the neutral player's: anyone may gather them
|
|
37
|
+
*/
|
|
38
|
+
export function base(world: World, player: number, at: Vec2, options: { workers?: number; hall?: string; wood?: boolean } = {}): Base {
|
|
39
|
+
const { workers = 5, hall: hallType = RTS.townhall, wood = true } = options
|
|
40
|
+
const hall = world.spawn(hallType, { at, owner: player })
|
|
41
|
+
const mine = world.spawn(RTS.goldmine, { at: { x: at.x - 9, z: at.z }, owner: 0 })
|
|
42
|
+
if (wood) forest(world, { x: at.x - 10, z: at.z + 6 }, 16, 3)
|
|
43
|
+
const peasants = Array.from({ length: workers }, (_, i) =>
|
|
44
|
+
world.spawn(RTS.peasant, { at: { x: at.x - 3 + (i % 3) * 1.2, z: at.z - 3 - Math.floor(i / 3) * 1.2 }, owner: player })
|
|
45
|
+
)
|
|
46
|
+
return { hall, mine, workers: peasants }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A wood: `width` by `depth` trees from `from`, a cell apart, some left out at random so its edge is ragged */
|
|
50
|
+
export function forest(world: World, from: Vec2, width: number, depth: number): void {
|
|
51
|
+
for (let z = 0; z < depth; z++)
|
|
52
|
+
for (let x = 0; x < width; x++) {
|
|
53
|
+
const at = { x: Math.floor(from.x) + x + 0.5, z: Math.floor(from.z) + z + 0.5 }
|
|
54
|
+
if (z === depth - 1 && world.rng.next() < 0.4) continue
|
|
55
|
+
// a tree there already (a wood laid out again keeps its trees): left as it is
|
|
56
|
+
if (
|
|
57
|
+
world.spatial
|
|
58
|
+
.queryStatic(at.x, at.z, 0.1)
|
|
59
|
+
.some((each) => each.def.id === RTS.tree && Math.abs(each.pos.x - at.x) + Math.abs(each.pos.z - at.z) < 0.1)
|
|
60
|
+
)
|
|
61
|
+
continue
|
|
62
|
+
world.spawn(RTS.tree, { at, owner: 0 })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Sends workers to gather: the first `toGold` to the mine, the rest to the nearest tree each */
|
|
67
|
+
export function gather(world: World, workers: Entity[], mine: Entity, toGold: number): void {
|
|
68
|
+
const trees = ownedOf(world, 0, RTS.tree)
|
|
69
|
+
workers.forEach((worker, i) => {
|
|
70
|
+
if (i < toGold) return worker.order('harvest', mine)
|
|
71
|
+
const nearest = trees.reduce<Entity | undefined>(
|
|
72
|
+
(best, tree) =>
|
|
73
|
+
!best ||
|
|
74
|
+
exactMath.hypot(tree.pos.x - worker.pos.x, tree.pos.z - worker.pos.z) < exactMath.hypot(best.pos.x - worker.pos.x, best.pos.z - worker.pos.z)
|
|
75
|
+
? tree
|
|
76
|
+
: best,
|
|
77
|
+
undefined
|
|
78
|
+
)
|
|
79
|
+
if (nearest) worker.order('harvest', nearest)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Puts a player's idle peasants to work, `count` of them at most: `toGold` on the gold of the mine nearest them, the
|
|
85
|
+
* rest in the wood
|
|
86
|
+
*/
|
|
87
|
+
export function putToWork(world: World, player: number, toGold: number, count = Infinity): void {
|
|
88
|
+
const idle = ownedOf(world, player, RTS.peasant)
|
|
89
|
+
.filter((each) => !each.orders.length)
|
|
90
|
+
.slice(0, count)
|
|
91
|
+
const [first] = idle
|
|
92
|
+
if (!first) return
|
|
93
|
+
const away = (each: Entity) => exactMath.hypot(each.pos.x - first.pos.x, each.pos.z - first.pos.z)
|
|
94
|
+
const [mine] = ownedOf(world, 0, RTS.goldmine).sort((a, b) => away(a) - away(b))
|
|
95
|
+
if (mine) gather(world, idle, mine, toGold)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `putToWork` a moment after the world starts. Not at once: an order on a mine or a tree is refused while its side
|
|
100
|
+
* has not seen it yet, and the world works out what each side sees as its first tick ends
|
|
101
|
+
*/
|
|
102
|
+
export const toWork = defineTimer<{ player: number; toGold: number; count: number }>({
|
|
103
|
+
id: 'rts.toWork',
|
|
104
|
+
run: (world, { player, toGold, count }) => putToWork(world, player, toGold, count)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
/** Puts the player's peasants to work shortly: see `toWork` */
|
|
108
|
+
export const startWork = (world: World, player: number, toGold: number, count = 99): void =>
|
|
109
|
+
void world.timers.after(0.2, toWork, { player, toGold, count })
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Queues a unit type at the player's least busy building that trains it, as the train button of its card does (the
|
|
113
|
+
* engine's `train` command): paid for as it is queued, refused when the player is short of it or of supply
|
|
114
|
+
*/
|
|
115
|
+
export const train = defineCommand('rts.train', { unitType: string }, (world, player, { unitType }) => {
|
|
116
|
+
const buildings = ownedOf(world, player).filter((each) => each.def.trains?.includes(unitType))
|
|
117
|
+
if (buildings.length) world.issue({ type: 'train', player, buildings: buildings.map((each) => each.id), unitType })
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
/** Researches an upgrade at the player's building that researches it, as its card's button does */
|
|
121
|
+
export const research = defineCommand('rts.research', { upgrade: string }, (world, player, { upgrade }) => {
|
|
122
|
+
const buildings = ownedOf(world, player).filter((each) => each.def.researches?.includes(upgrade))
|
|
123
|
+
if (buildings.length) world.issue({ type: 'research', player, buildings: buildings.map((each) => each.id), upgrade })
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/** Every idle worker of the player's back to work: half to the gold, half to the wood */
|
|
127
|
+
export const backToWork = defineCommand('rts.backToWork', {}, (world, player) => {
|
|
128
|
+
putToWork(world, player, Math.ceil(ownedOf(world, player, RTS.peasant).filter((each) => !each.orders.length).length / 2))
|
|
129
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building: a worker `builds` building types. Told to build one at a spot, it walks there, the site is laid out and
|
|
3
|
+
* paid for, and it hammers it up over the building's `buildTime`, the building rising as it goes. Placing one yourself
|
|
4
|
+
* is the worker's card: select a peasant, pick a building, click the ground (green where it fits, red where it does not)
|
|
5
|
+
*/
|
|
6
|
+
import { defineCommand, oneOf, type World } from '@v12-engine/engine'
|
|
7
|
+
import { PLAYER } from '../common/units'
|
|
8
|
+
import { type ExampleDef, exampleField } from '../define'
|
|
9
|
+
import { base, clear, ownedOf, purse, startWork } from './base'
|
|
10
|
+
import { RTS } from './data'
|
|
11
|
+
|
|
12
|
+
/** What the card's buttons build, and the spots each goes on in turn: a row of farms, a barracks, towers either side */
|
|
13
|
+
const SPOTS = {
|
|
14
|
+
[RTS.farm]: [
|
|
15
|
+
{ x: 20, z: 16 },
|
|
16
|
+
{ x: 23, z: 16 },
|
|
17
|
+
{ x: 26, z: 16 },
|
|
18
|
+
{ x: 29, z: 16 }
|
|
19
|
+
],
|
|
20
|
+
[RTS.barracks]: [
|
|
21
|
+
{ x: 34.5, z: 22.5 },
|
|
22
|
+
{ x: 34.5, z: 27.5 }
|
|
23
|
+
],
|
|
24
|
+
[RTS.tower]: [
|
|
25
|
+
{ x: 22, z: 11 },
|
|
26
|
+
{ x: 30, z: 11 }
|
|
27
|
+
]
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
/** The base with three peasants, two of them at work, and enough to build all the card has */
|
|
31
|
+
function layOut(world: World): void {
|
|
32
|
+
clear(world)
|
|
33
|
+
purse(world, PLAYER, 1500, 800)
|
|
34
|
+
// four peasants: two at work, one on the gold and one in the wood, and two idle by the hall, the builders
|
|
35
|
+
base(world, PLAYER, { x: 26, z: 24 }, { workers: 4 })
|
|
36
|
+
startWork(world, PLAYER, 1, 2)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A peasant told to build `unitType` on its next spot: the one nearest, idle ones first. A barracks must stand before a
|
|
41
|
+
* tower can (`requires`); short of gold or lumber, nothing is laid out and the player is told why
|
|
42
|
+
*/
|
|
43
|
+
export const build = defineCommand('rts.build', { unitType: oneOf(RTS.farm, RTS.barracks, RTS.tower) }, (world, player, { unitType }) => {
|
|
44
|
+
const spots = SPOTS[unitType]
|
|
45
|
+
const spot = spots[ownedOf(world, player, unitType).length]
|
|
46
|
+
// the builders: the two that do not gather, a barracks for the one and the rest for the other. What it is told
|
|
47
|
+
// while it builds waits its turn (`queue`)
|
|
48
|
+
const builders = ownedOf(world, player, RTS.peasant).filter((each) => each.orders[0]?.type !== 'harvest')
|
|
49
|
+
const builder = builders[unitType === RTS.barracks ? 1 : 0] ?? builders[0]
|
|
50
|
+
if (spot && builder) builder.order('build', { unitType, point: spot }, { queue: true })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export const resetBuild = defineCommand('rts.resetBuild', {}, (world) => layOut(world))
|
|
54
|
+
|
|
55
|
+
export const building: ExampleDef = {
|
|
56
|
+
id: 'rts-build',
|
|
57
|
+
category: 'rts',
|
|
58
|
+
title: 'Building a base',
|
|
59
|
+
summary:
|
|
60
|
+
'Peasants walk to a spot, lay out a building and hammer it up as it rises. Press a button to build a farm, a barracks or a tower, or select a peasant and place one yourself from its card.',
|
|
61
|
+
tags: ['build', 'construction', 'placement', 'worker', 'base', 'farm', 'barracks', 'tower'],
|
|
62
|
+
map: () => exampleField({ name: 'Building site', size: 44 }),
|
|
63
|
+
start: (world) => layOut(world),
|
|
64
|
+
source: ['examples/rts/build.ts', 'examples/rts/base.ts', 'examples/rts/data.ts', 'examples/rts/looks.ts']
|
|
65
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutral camps the RTS examples share: creeps that guard a spot (with the heroes pack), for the hero to hunt and the
|
|
3
|
+
* army to clear. They are the map's (`GameMap.camps`): a map with camps has the creeps' player, and its world puts them
|
|
4
|
+
* down as it is made. Laid out again, a camp is brought back where it was (`respawnCamp`)
|
|
5
|
+
*/
|
|
6
|
+
import { campsOf, type GameMap, MAP_CREEPS, respawnCamp, spawnCamp, type Vec2, type World } from '@v12-engine/engine'
|
|
7
|
+
|
|
8
|
+
/** A camp: its name, where it stands and who is in it */
|
|
9
|
+
export interface CampSpot {
|
|
10
|
+
name: string
|
|
11
|
+
at: Vec2
|
|
12
|
+
types: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The map with the camps on it, by name */
|
|
16
|
+
export function withCamps(map: GameMap, spots: readonly CampSpot[]): GameMap {
|
|
17
|
+
map.camps.push(...spots.map((spot, i) => ({ id: i + 1, name: spot.name, x: spot.at.x, z: spot.at.z, units: [...spot.types] })))
|
|
18
|
+
return map
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The camps, each where it was: new ones the first time, the same brought back afterwards (what stood there taken away first) */
|
|
22
|
+
export function camps(world: World, spots: readonly CampSpot[]): void {
|
|
23
|
+
const all = campsOf(world)
|
|
24
|
+
for (const spot of spots) {
|
|
25
|
+
const camp = all.find((each) => each.name === spot.name)
|
|
26
|
+
if (!camp) {
|
|
27
|
+
spawnCamp(world, MAP_CREEPS, spot.at, spot.types).name = spot.name
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
for (const id of camp.members) {
|
|
31
|
+
const creep = world.get(id)
|
|
32
|
+
if (creep?.alive) world.kill(creep, undefined, false, 'removed')
|
|
33
|
+
}
|
|
34
|
+
respawnCamp(world, camp)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What carries others (`UnitTypeDef.transport`): a ship on the water, a zeppelin in the air, a tower soldiers go into.
|
|
3
|
+
* Units of its side get in when told to (a right-click on it: the `board` order), are out of the world while inside, and
|
|
4
|
+
* get out on the land nearest it (the `unload` order, the card's Unload button). What a ship or a zeppelin carries dies
|
|
5
|
+
* with it; what a building holds comes out when it falls. The examples' cards send `carry`, as a player's clicks would
|
|
6
|
+
*/
|
|
7
|
+
import { defineCommand, defineComponent, type Entity, oneOf, type UnitTypeDef, type Vec2, type World } from '@v12-engine/engine'
|
|
8
|
+
import { UNIT } from '../common/units'
|
|
9
|
+
import { ownedOf } from './base'
|
|
10
|
+
|
|
11
|
+
/** The transports' ids */
|
|
12
|
+
export const CARRY = { ship: 'rtsTransportShip', zeppelin: 'rtsZeppelin', tower: 'rtsGarrisonTower' } as const
|
|
13
|
+
|
|
14
|
+
/** Who rides: the shared units that walk */
|
|
15
|
+
const riders: readonly string[] = [UNIT.footman, UNIT.archer, UNIT.mage, UNIT.knight]
|
|
16
|
+
|
|
17
|
+
/** A transport ship: it goes on deep water and nowhere else (`naval`), and takes eight on board */
|
|
18
|
+
export const ship: UnitTypeDef = {
|
|
19
|
+
id: CARRY.ship,
|
|
20
|
+
name: 'Transport Ship',
|
|
21
|
+
kind: 'unit',
|
|
22
|
+
radius: 1.6,
|
|
23
|
+
naval: true,
|
|
24
|
+
transport: { capacity: 8, takes: (unit) => riders.includes(unit.id) },
|
|
25
|
+
stats: { health: 900, armor: 10, moveSpeed: 4.5 },
|
|
26
|
+
sight: 11,
|
|
27
|
+
// a ship comes about slowly
|
|
28
|
+
turnRate: 2.2
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A zeppelin: it flies (`flying`), over cliffs and water alike, and takes eight up with it */
|
|
32
|
+
export const zeppelin: UnitTypeDef = {
|
|
33
|
+
id: CARRY.zeppelin,
|
|
34
|
+
name: 'Zeppelin',
|
|
35
|
+
kind: 'unit',
|
|
36
|
+
radius: 1.5,
|
|
37
|
+
flying: true,
|
|
38
|
+
transport: { capacity: 8, takes: (unit) => riders.includes(unit.id) },
|
|
39
|
+
stats: { health: 600, armor: 5, moveSpeed: 4.2 },
|
|
40
|
+
sight: 12,
|
|
41
|
+
turnRate: 1.8
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A tower soldiers go into: four of them, out of reach inside. It shoots only while someone is in it, and each one
|
|
46
|
+
* inside adds 12 to its arrows (`transport.armed`)
|
|
47
|
+
*/
|
|
48
|
+
export const garrisonTower: UnitTypeDef = {
|
|
49
|
+
id: CARRY.tower,
|
|
50
|
+
name: 'Garrison Tower',
|
|
51
|
+
kind: 'building',
|
|
52
|
+
radius: 1,
|
|
53
|
+
footprint: { w: 2, d: 2 },
|
|
54
|
+
transport: { capacity: 4, takes: (unit) => riders.includes(unit.id), armed: 12 },
|
|
55
|
+
stats: { health: 1800, attackDamage: 4, attackSpeed: 1.1, attackRange: 9, armor: 30 },
|
|
56
|
+
sight: 13,
|
|
57
|
+
attacks: [{ damagePoint: 0.3, acquisitionRange: 9, projectileSpeed: 26, targets: ['ground', 'air'] }]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const carryUnits: UnitTypeDef[] = [ship, zeppelin, garrisonTower]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where an example's transport goes, kept in the world (a component of its state) as each example lays itself out:
|
|
64
|
+
* where it waits for its passengers, where it goes across to, and where it lets them out over there
|
|
65
|
+
*/
|
|
66
|
+
export interface Route {
|
|
67
|
+
/** Where it waits by the passengers' side: on the water by the near bank, in the air over them */
|
|
68
|
+
moor: Vec2
|
|
69
|
+
/** Where it goes across to */
|
|
70
|
+
across: Vec2
|
|
71
|
+
/** Where it lets them out on the far side */
|
|
72
|
+
land: Vec2
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const route = defineComponent<Route>({
|
|
76
|
+
id: 'rts.route',
|
|
77
|
+
default: () => ({ moor: { x: 0, z: 0 }, across: { x: 0, z: 0 }, land: { x: 0, z: 0 } })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
/** The player's transport: their ship, zeppelin or tower */
|
|
81
|
+
export const transportOf = (world: World, player: number): Entity | undefined =>
|
|
82
|
+
ownedOf(world, player).find((each) => each.def.transport && !each.hidden)
|
|
83
|
+
|
|
84
|
+
/** The player's units that could get in: what walks and is not inside already */
|
|
85
|
+
export const ridersOf = (world: World, player: number): Entity[] =>
|
|
86
|
+
ownedOf(world, player).filter((each) => !each.hidden && riders.includes(each.def.id))
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* What the cards send: everyone gets in (`board`), the transport goes across or back (`move`), lets them out on the
|
|
90
|
+
* far side (`unload` at a point: it goes there first) or where it is, or it is sunk (killed, and its passengers with it)
|
|
91
|
+
*/
|
|
92
|
+
export const carry = defineCommand('rts.carry', { step: oneOf('board', 'across', 'unload', 'back', 'sink') }, (world, player, { step }) => {
|
|
93
|
+
const transport = transportOf(world, player)
|
|
94
|
+
if (!transport) return
|
|
95
|
+
const { across, land, moor } = world.state(route)
|
|
96
|
+
const order = (entities: Entity[], init: Record<string, unknown>) =>
|
|
97
|
+
world.issue({ type: 'order', player, entities: entities.map((each) => each.id), order: init as never })
|
|
98
|
+
if (step === 'board') order(ridersOf(world, player), { type: 'board', target: transport.id })
|
|
99
|
+
else if (step === 'across') order([transport], { type: 'move', point: across })
|
|
100
|
+
else if (step === 'back') order([transport], { type: 'move', point: moor })
|
|
101
|
+
else if (step === 'unload') order([transport], transport.isStructure ? { type: 'unload' } : { type: 'unload', point: land })
|
|
102
|
+
else world.kill(transport)
|
|
103
|
+
})
|