@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.
Files changed (263) hide show
  1. package/README.md +4 -1
  2. package/base/_gitignore +5 -0
  3. package/base/package.json +3 -3
  4. package/base/src/main.ts +36 -10
  5. package/base/src/navigation.worker.ts +7 -0
  6. package/base/vite.config.ts +24 -2
  7. package/cli.mjs +21 -6
  8. package/index.mjs +27 -7
  9. package/layers/desktop/electron/app.mjs +237 -0
  10. package/layers/desktop/electron/config.mjs +16 -0
  11. package/layers/desktop/electron/dev.mjs +63 -0
  12. package/layers/desktop/electron/files.mjs +86 -0
  13. package/layers/desktop/electron/icons/icon.icns +0 -0
  14. package/layers/desktop/electron/icons/icon.png +0 -0
  15. package/layers/desktop/electron/main.mjs +12 -0
  16. package/layers/desktop/electron/package.mjs +75 -0
  17. package/layers/desktop/electron/preload.cjs +41 -0
  18. package/layers/desktop/electron/presentation.m +87 -0
  19. package/layers/desktop/electron/serve.mjs +76 -0
  20. package/layers/desktop/electron/tools.mjs +63 -0
  21. package/layers/desktop/electron/updates.mjs +71 -0
  22. package/layers/desktop/package.json +13 -0
  23. package/layers/desktop/src/desktop.ts +175 -0
  24. package/layers/desktop/src/shell.ts +279 -0
  25. package/layers/desktop/tests/main.test.ts +189 -0
  26. package/layers/desktop/tests/shell.test.ts +152 -0
  27. package/layers/rooms/server.ts +13 -10
  28. package/layers/server/server.ts +4 -5
  29. package/layers/source/vite.config.ts +21 -1
  30. package/package.json +2 -1
  31. package/starters/campaign/README.md +9 -2
  32. package/starters/campaign/layers/rooms/server.ts +3 -3
  33. package/starters/campaign/layers/server/server.ts +3 -5
  34. package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
  35. package/starters/campaign/src/campaign/game.ts +26 -5
  36. package/starters/campaign/src/campaign/index.ts +104 -68
  37. package/starters/campaign/src/campaign/kills.ts +1 -1
  38. package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
  39. package/starters/campaign/src/campaign/missions/siege.ts +6 -6
  40. package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
  41. package/starters/campaign/src/campaign/scenes.ts +8 -7
  42. package/starters/campaign/src/campaign/units.ts +59 -20
  43. package/starters/campaign/src/campaign/view/attacks.ts +19 -0
  44. package/starters/campaign/src/campaign/view/fire.ts +2 -2
  45. package/starters/campaign/src/campaign/view/index.ts +54 -43
  46. package/starters/campaign/src/campaign/waves.ts +18 -17
  47. package/starters/campaign/src/game.ts +8 -1
  48. package/starters/campaign/src/main.ts +38 -19
  49. package/starters/campaign/src/maps/fort.names.ts +2 -2
  50. package/starters/campaign/src/maps/fort.vgmap.json +102 -2
  51. package/starters/campaign/src/maps/homestead.names.ts +2 -2
  52. package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
  53. package/starters/campaign/src/maps/pass.names.ts +2 -2
  54. package/starters/campaign/src/maps/pass.vgmap.json +69 -1
  55. package/starters/campaign/src/waves-panel.ts +2 -1
  56. package/starters/empty/README.md +18 -10
  57. package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
  58. package/starters/empty/src/game.ts +11 -75
  59. package/starters/empty/src/generate.worker.ts +5 -5
  60. package/starters/empty/src/rules.ts +83 -25
  61. package/starters/empty/src/view.ts +82 -0
  62. package/starters/examples/README.md +83 -0
  63. package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
  64. package/starters/examples/package.json +5 -0
  65. package/starters/examples/src/examples/abilities/areas.ts +165 -0
  66. package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
  67. package/starters/examples/src/examples/abilities/buffs.ts +287 -0
  68. package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
  69. package/starters/examples/src/examples/abilities/changing.ts +228 -0
  70. package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
  71. package/starters/examples/src/examples/abilities/control.ts +158 -0
  72. package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
  73. package/starters/examples/src/examples/abilities/damage.ts +156 -0
  74. package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
  75. package/starters/examples/src/examples/abilities/family.ts +29 -0
  76. package/starters/examples/src/examples/abilities/index.ts +32 -0
  77. package/starters/examples/src/examples/abilities/kit.ts +179 -0
  78. package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
  79. package/starters/examples/src/examples/abilities/looks.ts +47 -0
  80. package/starters/examples/src/examples/abilities/missiles.ts +203 -0
  81. package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
  82. package/starters/examples/src/examples/abilities/moves.ts +206 -0
  83. package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
  84. package/starters/examples/src/examples/abilities/recasts.ts +181 -0
  85. package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
  86. package/starters/examples/src/examples/abilities/resources.ts +166 -0
  87. package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
  88. package/starters/examples/src/examples/abilities/summons.ts +296 -0
  89. package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
  90. package/starters/examples/src/examples/abilities/triggers.ts +204 -0
  91. package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
  92. package/starters/examples/src/examples/abilities/vision.ts +137 -0
  93. package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
  94. package/starters/examples/src/examples/animation/index.ts +235 -0
  95. package/starters/examples/src/examples/animation/looks.ts +444 -0
  96. package/starters/examples/src/examples/catalog.ts +52 -0
  97. package/starters/examples/src/examples/cinematics/index.ts +217 -0
  98. package/starters/examples/src/examples/cinematics/looks.ts +476 -0
  99. package/starters/examples/src/examples/cinematics/tour.json +43 -0
  100. package/starters/examples/src/examples/cinematics/village.ts +88 -0
  101. package/starters/examples/src/examples/common/looks.ts +65 -0
  102. package/starters/examples/src/examples/common/units.ts +70 -0
  103. package/starters/examples/src/examples/construction/index.ts +329 -0
  104. package/starters/examples/src/examples/construction/looks.ts +275 -0
  105. package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
  106. package/starters/examples/src/examples/custom-animation/index.ts +190 -0
  107. package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
  108. package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
  109. package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
  110. package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
  111. package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
  112. package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
  113. package/starters/examples/src/examples/custom-fx/index.ts +25 -0
  114. package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
  115. package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
  116. package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
  117. package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
  118. package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
  119. package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
  120. package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
  121. package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
  122. package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
  123. package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
  124. package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
  125. package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
  126. package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
  127. package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
  128. package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
  129. package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
  130. package/starters/examples/src/examples/define.ts +86 -0
  131. package/starters/examples/src/examples/destruction/index.ts +570 -0
  132. package/starters/examples/src/examples/destruction/looks.ts +447 -0
  133. package/starters/examples/src/examples/dialogs/index.ts +383 -0
  134. package/starters/examples/src/examples/dialogs/looks.ts +546 -0
  135. package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
  136. package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
  137. package/starters/examples/src/examples/fx/blood.look.ts +62 -0
  138. package/starters/examples/src/examples/fx/blood.ts +59 -0
  139. package/starters/examples/src/examples/fx/feel.look.ts +86 -0
  140. package/starters/examples/src/examples/fx/feel.ts +57 -0
  141. package/starters/examples/src/examples/fx/fight.ts +28 -0
  142. package/starters/examples/src/examples/fx/floats.look.ts +108 -0
  143. package/starters/examples/src/examples/fx/floats.ts +24 -0
  144. package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
  145. package/starters/examples/src/examples/fx/gallery.ts +23 -0
  146. package/starters/examples/src/examples/fx/index.ts +30 -0
  147. package/starters/examples/src/examples/fx/links.look.ts +83 -0
  148. package/starters/examples/src/examples/fx/links.ts +49 -0
  149. package/starters/examples/src/examples/fx/looks.ts +55 -0
  150. package/starters/examples/src/examples/fx/map.look.ts +93 -0
  151. package/starters/examples/src/examples/fx/map.ts +33 -0
  152. package/starters/examples/src/examples/fx/marks.look.ts +100 -0
  153. package/starters/examples/src/examples/fx/marks.ts +36 -0
  154. package/starters/examples/src/examples/fx/shots.look.ts +92 -0
  155. package/starters/examples/src/examples/fx/shots.ts +71 -0
  156. package/starters/examples/src/examples/fx/spells.look.ts +87 -0
  157. package/starters/examples/src/examples/fx/spells.ts +122 -0
  158. package/starters/examples/src/examples/fx/stage.ts +31 -0
  159. package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
  160. package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
  161. package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
  162. package/starters/examples/src/examples/gui/index.ts +143 -0
  163. package/starters/examples/src/examples/gui/keys.ts +91 -0
  164. package/starters/examples/src/examples/gui/layout.ts +42 -0
  165. package/starters/examples/src/examples/gui/looks.ts +270 -0
  166. package/starters/examples/src/examples/gui/minimap.ts +127 -0
  167. package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
  168. package/starters/examples/src/examples/gui/plates.ts +87 -0
  169. package/starters/examples/src/examples/gui/radial.ts +104 -0
  170. package/starters/examples/src/examples/gui/solidPart.ts +107 -0
  171. package/starters/examples/src/examples/looks.ts +45 -0
  172. package/starters/examples/src/examples/mechanics/battle.ts +51 -0
  173. package/starters/examples/src/examples/mechanics/damage.ts +80 -0
  174. package/starters/examples/src/examples/mechanics/flying.ts +93 -0
  175. package/starters/examples/src/examples/mechanics/formations.ts +58 -0
  176. package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
  177. package/starters/examples/src/examples/mechanics/index.ts +44 -0
  178. package/starters/examples/src/examples/mechanics/looks.ts +289 -0
  179. package/starters/examples/src/examples/mechanics/maze.ts +119 -0
  180. package/starters/examples/src/examples/mechanics/orders.ts +68 -0
  181. package/starters/examples/src/examples/mechanics/selection.ts +30 -0
  182. package/starters/examples/src/examples/moba/index.ts +377 -0
  183. package/starters/examples/src/examples/moba/looks.ts +349 -0
  184. package/starters/examples/src/examples/moba/maps.ts +69 -0
  185. package/starters/examples/src/examples/moba/spells.ts +34 -0
  186. package/starters/examples/src/examples/moba/units.ts +196 -0
  187. package/starters/examples/src/examples/nature/camera.ts +15 -0
  188. package/starters/examples/src/examples/nature/grass.ts +64 -0
  189. package/starters/examples/src/examples/nature/index.ts +327 -0
  190. package/starters/examples/src/examples/nature/land.ts +173 -0
  191. package/starters/examples/src/examples/nature/looks.ts +577 -0
  192. package/starters/examples/src/examples/nature/maps.ts +370 -0
  193. package/starters/examples/src/examples/nature/skies.ts +324 -0
  194. package/starters/examples/src/examples/nature/weather.ts +80 -0
  195. package/starters/examples/src/examples/performance/index.ts +263 -0
  196. package/starters/examples/src/examples/performance/looks.ts +372 -0
  197. package/starters/examples/src/examples/performance/meter.ts +158 -0
  198. package/starters/examples/src/examples/post/grades.ts +86 -0
  199. package/starters/examples/src/examples/post/index.ts +202 -0
  200. package/starters/examples/src/examples/post/looks.ts +884 -0
  201. package/starters/examples/src/examples/post/passes.ts +213 -0
  202. package/starters/examples/src/examples/rts/ai.ts +49 -0
  203. package/starters/examples/src/examples/rts/airlift.ts +43 -0
  204. package/starters/examples/src/examples/rts/base.ts +129 -0
  205. package/starters/examples/src/examples/rts/build.ts +65 -0
  206. package/starters/examples/src/examples/rts/camps.ts +36 -0
  207. package/starters/examples/src/examples/rts/carry.ts +103 -0
  208. package/starters/examples/src/examples/rts/creeps.ts +61 -0
  209. package/starters/examples/src/examples/rts/data.ts +406 -0
  210. package/starters/examples/src/examples/rts/ferry.ts +41 -0
  211. package/starters/examples/src/examples/rts/garrison.ts +50 -0
  212. package/starters/examples/src/examples/rts/harvest.ts +29 -0
  213. package/starters/examples/src/examples/rts/hero.ts +67 -0
  214. package/starters/examples/src/examples/rts/index.ts +77 -0
  215. package/starters/examples/src/examples/rts/looks.ts +721 -0
  216. package/starters/examples/src/examples/rts/shop.ts +46 -0
  217. package/starters/examples/src/examples/rts/tech.ts +39 -0
  218. package/starters/examples/src/examples/rts/train.ts +48 -0
  219. package/starters/examples/src/examples/rts/waterside.ts +185 -0
  220. package/starters/examples/src/examples/showcase/index.ts +714 -0
  221. package/starters/examples/src/examples/showcase/looks.ts +238 -0
  222. package/starters/examples/src/game.ts +17 -0
  223. package/starters/examples/src/main.ts +82 -0
  224. package/starters/examples/src/rules.ts +31 -0
  225. package/starters/examples/src/shell/look.ts +79 -0
  226. package/starters/examples/src/shell/registry.ts +7 -0
  227. package/starters/examples/src/shell/scenes.ts +178 -0
  228. package/starters/examples/src/shell/shell.ts +290 -0
  229. package/starters/examples/src/shell/style.ts +202 -0
  230. package/starters/examples/src/view.ts +50 -0
  231. package/starters/melee/README.md +12 -3
  232. package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
  233. package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
  234. package/starters/melee/src/game.ts +32 -2
  235. package/starters/melee/src/maps/bay.names.ts +2 -2
  236. package/starters/melee/src/maps/bay.vgmap.json +198 -1
  237. package/starters/melee/src/maps.ts +11 -9
  238. package/starters/melee/src/melee/abilities.ts +1 -194
  239. package/starters/melee/src/melee/blight.ts +7 -7
  240. package/starters/melee/src/melee/cellLayers.json +3 -3
  241. package/starters/melee/src/melee/data.json +1 -18
  242. package/starters/melee/src/melee/game.ts +6 -4
  243. package/starters/melee/src/melee/heroes.ts +1025 -0
  244. package/starters/melee/src/melee/index.ts +64 -61
  245. package/starters/melee/src/melee/items.ts +15 -362
  246. package/starters/melee/src/melee/languages/he.json +14 -2
  247. package/starters/melee/src/melee/pings.ts +5 -6
  248. package/starters/melee/src/melee/saves.ts +1 -1
  249. package/starters/melee/src/melee/undead.ts +15 -230
  250. package/starters/melee/src/melee/units.ts +37 -171
  251. package/starters/melee/src/melee/view/attacks.ts +19 -0
  252. package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
  253. package/starters/melee/src/melee/view/index.ts +56 -52
  254. package/starters/melee/src/melee/view/looks.ts +86 -63
  255. package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
  256. package/starters/melee/src/melee/view/walls.ts +1 -1
  257. package/starters.mjs +6 -0
  258. package/v12.mjs +14 -15
  259. package/starters/campaign/src/rules.ts +0 -20
  260. package/starters/empty/src/maps.ts +0 -44
  261. package/starters/empty/src/tiles.ts +0 -12
  262. package/starters/melee/src/melee/heroSpells.ts +0 -38
  263. 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
+ })