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