@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,546 @@
1
+ /**
2
+ * The dialogue examples on the client's side: who speaks and what their lines say (`cinematics.speakers`,
3
+ * `hud.messages`), the toll's cinematic with its choice, the dialog box moved and restyled (`client.setDialogLook`), a
4
+ * box and a choice menu of the game's own (`cinematics.dialog.mount`, `cinematics.choice.mount`), speech bubbles pinned
5
+ * over units (`client.attach`), and the ways the client tells the player things (`alert`, `ping`, `floats`)
6
+ */
7
+ import type { Cinematic, ChoiceProps, GameClientApi, GameView } from '@v12-engine/client'
8
+ import { button, type CategoryLooks, readout, select, slider, toggle } from '../../shell/look'
9
+ import { aim, villageFx } from '../cinematics/looks'
10
+ import { walk } from '../cinematics/index'
11
+ import { ROAD_END, SQUARE } from '../cinematics/village'
12
+ import { character, hold, person } from '../common/looks'
13
+ import { converse, ELDER_AT, news, resetToll, SPEAKER, TALK, TOLL_CINEMATIC, TOLL_QUESTION, wander } from './index'
14
+
15
+ /** What a dialog box of the game's own is given (the client's `DialogProps`: the line, how much of it shows, `next`) */
16
+ type DialogBoxProps = Parameters<NonNullable<NonNullable<NonNullable<GameView['cinematics']>['dialog']>['mount']>>[1]
17
+
18
+ /** Who speaks: names, colours, and faces (a unit type's model, talking as the line is said) */
19
+ const SPEAKERS = {
20
+ [SPEAKER.knight]: { name: 'Sir Aldwin', color: '#9cc4ff', portrait: { type: 'knight' } },
21
+ [SPEAKER.elder]: { name: 'Elder Maren', color: '#e8c27a', portrait: { type: TALK['dialogs.elder'] } },
22
+ [SPEAKER.bandit]: { name: 'Bandit', color: '#ff8a65', portrait: { type: 'cinematics.raider' } }
23
+ }
24
+
25
+ /** The words of the rules' lines and messages, by their keys: the rules say keys, the screen has the words */
26
+ const MESSAGES: Record<string, string> = {
27
+ dialogsTalk1: 'You came after all. The road from the capital is long, and not kind to travellers.',
28
+ dialogsTalk2: 'I came as soon as your letter reached me, Elder.',
29
+ dialogsTalk3: 'Then you know of the lights in the east woods. Every night, closer to the wall.',
30
+ dialogsTalk4: 'Lights are only lights. Who carries them?',
31
+ dialogsTalk5: 'That, Sir Aldwin, is what I hoped you would find out.',
32
+ dialogsPaid: 'You paid the toll: 50 gold. The bandits melt into the woods.',
33
+ dialogsFight: 'The bandits draw their axes!',
34
+ dialogsJoined: 'Your words win them over: the bandits follow you now.',
35
+ dialogsQuestBandits: 'Clear the bandits from the east woods',
36
+ dialogsQuestBanditsDetail: 'Three of them, by the old cart track past the inn.',
37
+ dialogsQuestBack: 'Return to Elder Maren',
38
+ dialogsQuest1: 'Sir Aldwin! Bandits have made camp in the east woods. They take our sheep and frighten the children.',
39
+ dialogsQuest2: 'How many?',
40
+ dialogsQuest3: 'Three, we think. Drive them off, and Elmbrook will not forget it.',
41
+ dialogsQuestBackLine: 'That is the last of them. Back to the elder.',
42
+ dialogsQuest4: 'The woods are quiet again. Take this, with the thanks of every soul here.',
43
+ dialogsQuest5: 'Keep the fire lit, Elder. I will be back.',
44
+ dialogsQuestReward: 'Quest complete: 500 experience',
45
+ dialogsNews_info: 'A merchant caravan has arrived at the market.',
46
+ dialogsNews_hint: 'Hint: villagers with a ! over their heads have something to say.',
47
+ dialogsNews_error: 'Not enough gold.',
48
+ dialogsNewsObjective: 'Find the merchant at the market',
49
+ dialogsNewsLine: 'Mind the wolves on the north road tonight.'
50
+ }
51
+
52
+ // ---- A choice that matters
53
+
54
+ /**
55
+ * The toll: the camera on the bandits' leader, his demand, the knight's look, and the question. Played by its id (the
56
+ * view's `cinematics.defs`), so the answer goes to the rules as a command (`cinematicChoice`), which branch on it
57
+ */
58
+ const tollScene: Cinematic = async (cut) => {
59
+ const leader = cut.entity('bandit2')
60
+ const hero = cut.entity('hero')
61
+ if (leader === undefined || hero === undefined) return
62
+ await cut.shot({ at: { entity: leader }, distance: 7, yaw: 0.25, pitch: 0.2, fov: 42, height: 1.3 }, { seconds: 1, ease: 'inOutCubic' })
63
+ await cut.say(SPEAKER.bandit, 'Far enough, knight. This road has a toll. Fifty gold, or your shiny armour.', 4.5)
64
+ await cut.shot({ at: { entity: hero }, distance: 6, yaw: Math.PI - 0.25, pitch: 0.18, fov: 42, height: 1.5 }, { seconds: 0.4 })
65
+ await cut.shot({ at: { x: ROAD_END.x, z: ROAD_END.z + 10 }, distance: 14, yaw: 1.2, pitch: 0.35 }, { seconds: 1.2, ease: 'inOutSine' })
66
+ const answer = await cut.choose(
67
+ [
68
+ { id: 'pay', label: 'Pay the fifty gold' },
69
+ { id: 'fight', label: 'Draw your sword' },
70
+ { id: 'talk', label: 'You look hungry. Ride with me instead.' }
71
+ ],
72
+ { name: TOLL_QUESTION, question: 'The bandits wait for your answer.', seconds: 20, default: 'pay' }
73
+ )
74
+ const reply = {
75
+ pay: 'Take it, and be gone.',
76
+ fight: 'My sword is the only toll you will get.',
77
+ talk: 'There is honest work in Elmbrook, and food.'
78
+ }
79
+ await cut.say(SPEAKER.knight, reply[answer as keyof typeof reply] ?? '…', 3)
80
+ }
81
+
82
+ /** Lays the toll out again and plays it, a moment later (the new units land at the next tick) */
83
+ function meetTheBandits(client: GameClientApi): void {
84
+ client.session.send(resetToll.make({}))
85
+ setTimeout(() => void client.play(TOLL_CINEMATIC, { hold: true }), 300)
86
+ }
87
+
88
+ // ---- Two talking
89
+
90
+ /** How the conversation's box looks, as the controls set it */
91
+ const box: { position: 'left' | 'right' | 'bottom' | 'top' | 'center'; subtitles: boolean; typewriter: number; portraitIn: 'hud' | 'box' } = {
92
+ position: 'left',
93
+ subtitles: false,
94
+ typewriter: 0,
95
+ portraitIn: 'box'
96
+ }
97
+ const lookOfBox = (client: GameClientApi) => client.setDialogLook({ ...box })
98
+
99
+ // ---- A quest giver
100
+
101
+ /**
102
+ * The quest's mark over the elder's head: a ! while she has a quest to give, a ? while it waits to be handed in,
103
+ * nothing else. Read from the quest log the HUD shows (`client.ui.objectives`), followed with `client.ui.watch`
104
+ */
105
+ function questMark(client: GameClientApi): () => void {
106
+ const elder = client.session.snapshot().entities.find((each) => each.name === 'elder')
107
+ if (!elder) return () => {}
108
+ // the pin moves the outer element (its transform); the mark bobs inside it
109
+ const pinned = document.createElement('div')
110
+ const mark = document.createElement('div')
111
+ mark.style.cssText =
112
+ 'font:900 34px Georgia,serif;color:#ffd23c;text-shadow:0 0 6px #000,0 0 14px rgba(255,190,40,.8);animation:dialogsBob 1.2s ease-in-out infinite alternate'
113
+ pinned.append(mark)
114
+ const style = document.createElement('style')
115
+ style.textContent = '@keyframes dialogsBob { from { transform: translateY(0) } to { transform: translateY(-6px) } }'
116
+ document.head.append(style)
117
+ const pin = client.attach(pinned, { entity: elder.id, height: 0.5 })
118
+ let pinged = false
119
+ const stop = client.ui.watch(
120
+ () => client.ui.objectives(),
121
+ (objectives) => {
122
+ const quest = objectives.find((each) => each.id === 'dialogs.bandits')
123
+ const back = objectives.find((each) => each.id === 'dialogs.back')
124
+ mark.textContent = !quest ? '!' : back?.status === 'open' ? '?' : ''
125
+ // the bandits' camp marked on the minimap as the quest is given
126
+ if (quest && !pinged) {
127
+ pinged = true
128
+ client.minimapPing(38, 31, '#ff5a3c', 6)
129
+ }
130
+ },
131
+ { immediate: true }
132
+ )
133
+ return () => {
134
+ stop()
135
+ pin.remove()
136
+ style.remove()
137
+ }
138
+ }
139
+
140
+ // ---- Barks
141
+
142
+ /** What the market's people say, now and then */
143
+ const CHATTER = [
144
+ 'Fresh apples! Two a copper!',
145
+ 'Did you hear about the lights in the woods?',
146
+ 'My back is killing me.',
147
+ 'Rain by nightfall, mark my words.',
148
+ 'Has anyone seen my goat?',
149
+ 'The bell rang twice last night…',
150
+ 'Bread! Still warm!',
151
+ 'Mind the cart!'
152
+ ]
153
+ /** What one says when the player clicks them, and what the guard says when ordered */
154
+ const CLICKED = ['Yes?', 'Can I help you?', 'Lovely day.', 'Hm?', 'Don’t mind me.']
155
+ const ORDERED = ['Right away!', 'As you say.', 'Moving.', 'On my way!', 'For Elmbrook!']
156
+
157
+ /** The barks' settings, as the controls set them */
158
+ const barks: { chatter: boolean; style: 'bubble' | 'floating words'; pick: number } = { chatter: true, style: 'bubble', pick: 0 }
159
+
160
+ /** A speech bubble over a unit's head for `seconds`: an element pinned to the unit, faded out and taken away */
161
+ function bubble(client: GameClientApi, id: number, text: string, seconds = 2.8): void {
162
+ if (barks.style === 'floating words') {
163
+ client.floats.show({ text, style: { color: '#fff4d6', size: 15 }, seconds }, { on: id })
164
+ return
165
+ }
166
+ const element = document.createElement('div')
167
+ element.textContent = text
168
+ element.style.cssText = `max-width:200px;padding:6px 10px;border-radius:12px;background:#fffaf0;color:#2a2116;font:600 13px/1.3 system-ui,sans-serif;
169
+ box-shadow:0 2px 8px rgba(0,0,0,.45);transition:opacity .4s;white-space:normal;text-align:center;filter:drop-shadow(0 1px 0 #c9b58f)`
170
+ const tail = document.createElement('div')
171
+ tail.style.cssText = 'position:absolute;left:50%;bottom:-7px;margin-left:-7px;border:7px solid transparent;border-bottom:0;border-top-color:#fffaf0'
172
+ element.append(tail)
173
+ const pin = client.attach(element, { entity: id, height: 0.4 }, { offset: { y: -6 } })
174
+ setTimeout(() => (element.style.opacity = '0'), seconds * 1000 - 400)
175
+ setTimeout(() => pin.remove(), seconds * 1000)
176
+ }
177
+
178
+ /** A line picked in turn from a list: the same order on every run, never the same twice running */
179
+ const next = (lines: readonly string[]) => lines[barks.pick++ % lines.length]
180
+
181
+ /** Someone of the market says something: a villager, picked in turn */
182
+ function chatter(client: GameClientApi): void {
183
+ const villagers = client.session.snapshot().entities.filter((each) => each.type === TALK['dialogs.villager'] && each.health > 0)
184
+ if (!villagers.length) return
185
+ bubble(client, villagers[barks.pick % villagers.length].id, next(CHATTER))
186
+ }
187
+
188
+ // ---- A dialog box of your own
189
+
190
+ /**
191
+ * A dialog box drawn by the game: a parchment scroll, the speaker's name in red ink, the words typed out as the
192
+ * client reveals them (`shown()`, read every frame), a click to move on (`next`)
193
+ */
194
+ function parchmentBox(element: HTMLElement, props: DialogBoxProps): () => void {
195
+ // laid at the foot of the picture, over the game: the element given is the client's place for the box
196
+ element.style.cssText = 'position:absolute;inset:0 0 36px 0;display:flex;justify-content:center;align-items:flex-end;pointer-events:none'
197
+ element.innerHTML = `<div style="pointer-events:auto;cursor:pointer;width:min(640px,90%);padding:18px 26px 16px 86px;position:relative;
198
+ background:linear-gradient(#f3e3bf,#e6cf9c);border:2px solid #8b6a3a;border-radius:6px;box-shadow:0 6px 24px rgba(0,0,0,.5),inset 0 0 30px rgba(120,80,30,.35);
199
+ font:17px/1.45 Georgia,serif;color:#3a2a14">
200
+ <div data-seal style="position:absolute;left:18px;top:16px;width:52px;height:52px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#d9534f,#8a1f1b);
201
+ color:#ffe8c8;font:700 24px Georgia,serif;display:grid;place-items:center;box-shadow:0 2px 5px rgba(0,0,0,.5)"></div>
202
+ <div data-name style="font:700 15px Georgia,serif;letter-spacing:.08em;text-transform:uppercase;color:#8a1f1b;margin-bottom:4px"></div>
203
+ <div data-words></div>
204
+ <div style="position:absolute;right:14px;bottom:6px;font:12px Georgia,serif;color:#8b6a3a">click ▸</div>
205
+ </div>`
206
+ const { line } = props
207
+ element.querySelector<HTMLElement>('[data-seal]')!.textContent = line.speaker.charAt(0)
208
+ element.querySelector<HTMLElement>('[data-name]')!.textContent = line.speaker
209
+ const words = element.querySelector<HTMLElement>('[data-words]')!
210
+ element.firstElementChild!.addEventListener('click', () => props.next())
211
+ let frame = 0
212
+ const type = () => {
213
+ words.textContent = line.text.slice(0, props.shown())
214
+ frame = requestAnimationFrame(type)
215
+ }
216
+ type()
217
+ return () => cancelAnimationFrame(frame)
218
+ }
219
+
220
+ /** A choice menu drawn by the game: the question, and each answer a wax seal with its key */
221
+ function sealChoice(element: HTMLElement, props: ChoiceProps): void {
222
+ const { menu } = props
223
+ element.style.cssText = 'position:absolute;inset:0 0 60px 0;display:flex;justify-content:center;align-items:flex-end;pointer-events:none'
224
+ element.innerHTML = `<div style="pointer-events:auto;display:flex;flex-direction:column;gap:8px;align-items:center;font:16px Georgia,serif">
225
+ <div data-question style="color:#f3e3bf;text-shadow:0 1px 4px #000;margin-bottom:4px"></div></div>`
226
+ element.querySelector<HTMLElement>('[data-question]')!.textContent = menu.question ?? ''
227
+ for (const option of menu.options) {
228
+ const answer = document.createElement('button')
229
+ answer.type = 'button'
230
+ answer.innerHTML = `<b style="display:inline-grid;place-items:center;width:26px;height:26px;margin-right:10px;border-radius:50%;background:#8a1f1b;color:#ffe8c8"></b><span></span>`
231
+ answer.querySelector('b')!.textContent = option.key
232
+ answer.querySelector('span')!.textContent = option.label
233
+ answer.style.cssText =
234
+ 'cursor:pointer;min-width:340px;text-align:left;padding:8px 16px;border:2px solid #8b6a3a;border-radius:6px;background:linear-gradient(#f3e3bf,#e6cf9c);color:#3a2a14;font:16px Georgia,serif;box-shadow:0 3px 10px rgba(0,0,0,.45)'
235
+ answer.onclick = () => props.pick(option.id)
236
+ element.firstElementChild!.append(answer)
237
+ }
238
+ }
239
+
240
+ /** The conversation in the parchment box, ending on a choice: a cinematic with no bars, the HUD kept */
241
+ const parchmentTalk: Cinematic = async (cut) => {
242
+ await cut.say(SPEAKER.elder, 'A letter came for you this morning, sealed in red. The rider would not wait.', { waits: true, pace: 45 })
243
+ await cut.say(SPEAKER.knight, 'Red wax. That is the Queen’s seal.', { waits: true, pace: 45 })
244
+ const answer = await cut.choose(
245
+ [
246
+ { id: 'open', label: 'Break the seal and read it' },
247
+ { id: 'keep', label: 'Keep it for later' }
248
+ ],
249
+ { name: 'letter', question: 'What do you do with the letter?' }
250
+ )
251
+ const reply = answer === 'open' ? 'It says only: come home. Something is wrong in the capital.' : 'Wise. Some letters are better read by daylight.'
252
+ await cut.say(answer === 'open' ? SPEAKER.knight : SPEAKER.elder, reply, { waits: true, pace: 45 })
253
+ }
254
+
255
+ /** The camera on the fire, close, where the two of them stand */
256
+ const atTheFire = (client: GameClientApi) => aim(client, { x: SQUARE.x, z: SQUARE.z + 0.5 }, 15)
257
+
258
+ export const category: CategoryLooks = {
259
+ // the speakers, the words, the elder's and the villagers' looks, and the toll's cinematic by its id
260
+ view: (view: GameView) => ({
261
+ ...view,
262
+ units: {
263
+ ...view.units,
264
+ looks: {
265
+ ...view.units.looks,
266
+ [TALK['dialogs.elder']]: { height: 1.8, model: character('mage', 0.68, { hold: [hold('staff')] }), shapes: person(1.8, [0.5, 0.3, 0.6]) },
267
+ [TALK['dialogs.villager']]: { height: 1.7, model: character('rogue', 0.66), shapes: person(1.7, [0.55, 0.45, 0.35]) }
268
+ }
269
+ },
270
+ hud: { ...view.hud, messages: { ...view.hud?.messages, ...MESSAGES } },
271
+ cinematics: {
272
+ ...view.cinematics,
273
+ defs: { ...view.cinematics?.defs, [TOLL_CINEMATIC]: { play: tollScene, back: true } },
274
+ speakers: { ...view.cinematics?.speakers, ...SPEAKERS }
275
+ }
276
+ }),
277
+ looks: {
278
+ 'dialogs-conversation': {
279
+ controls: [
280
+ button(
281
+ 'Talk',
282
+ (client) => client.session.send(converse.make({})),
283
+ 'The elder and the villager say five lines in turn, sent by the rules (talk, in a sequence): watch the box and the faces change.'
284
+ ),
285
+ select(
286
+ 'Box',
287
+ ['left', 'right', 'bottom', 'top', 'center'],
288
+ 'left',
289
+ (client, position) => {
290
+ box.position = position as typeof box.position
291
+ lookOfBox(client)
292
+ },
293
+ 'Where on the screen the dialog box sits: shows on the next line said (press Talk).'
294
+ ),
295
+ select(
296
+ 'Face',
297
+ ['in the box', 'in the HUD'],
298
+ 'in the box',
299
+ (client, where) => {
300
+ box.portraitIn = where === 'in the box' ? 'box' : 'hud'
301
+ lookOfBox(client)
302
+ },
303
+ 'Where the speaker’s face shows: beside the words in the box, or in the HUD’s portrait the classic RTS way.'
304
+ ),
305
+ toggle(
306
+ 'Subtitles',
307
+ false,
308
+ (client, on) => {
309
+ box.subtitles = on
310
+ lookOfBox(client)
311
+ },
312
+ 'Lines drawn as a film’s subtitles instead: a line of words at the foot of the picture, no box, no face.'
313
+ ),
314
+ slider(
315
+ 'Typewriter',
316
+ { min: 0, max: 80, step: 10, value: 0 },
317
+ (client, pace) => {
318
+ box.typewriter = pace
319
+ lookOfBox(client)
320
+ },
321
+ 'How many characters a second each line types out at: at 0 the whole line shows at once. Press Talk to see it.'
322
+ ),
323
+ readout(
324
+ 'Said so far',
325
+ (client) => `${client.dialog.history().length} lines`,
326
+ 'How many lines the dialog’s history keeps, all that was said since the example opened.'
327
+ )
328
+ ],
329
+ ready: (client) => {
330
+ atTheFire(client)
331
+ lookOfBox(client)
332
+ const out = villageFx(client)
333
+ const timer = setTimeout(() => client.session.send(converse.make({})), 800)
334
+ return () => {
335
+ clearTimeout(timer)
336
+ client.setDialogLook(null)
337
+ out()
338
+ }
339
+ }
340
+ },
341
+ 'dialogs-choice': {
342
+ controls: [
343
+ button(
344
+ 'Meet the bandits',
345
+ (client) => meetTheBandits(client),
346
+ 'Lays the road out again and plays the meeting: the bandits ask a toll, and what you choose (pay, fight, talk them round) decides how it goes.'
347
+ ),
348
+ readout(
349
+ 'Bandits',
350
+ (client) => {
351
+ const units = client.session.snapshot().entities.filter((each) => each.type === 'cinematics.raider' && each.health > 0)
352
+ const theirs = units.filter((each) => each.owner !== client.session.playerId).length
353
+ return `${theirs} against you · ${units.length - theirs} with you`
354
+ },
355
+ 'How many bandits are left on each side: talked round, they join you.'
356
+ )
357
+ ],
358
+ ready: (client) => {
359
+ aim(client, { x: ROAD_END.x, z: ROAD_END.z + 13 }, 22)
360
+ const out = villageFx(client)
361
+ const timer = setTimeout(() => void client.play(TOLL_CINEMATIC, { hold: true }), 800)
362
+ return () => {
363
+ clearTimeout(timer)
364
+ out()
365
+ }
366
+ }
367
+ },
368
+ 'dialogs-quest': {
369
+ controls: [
370
+ button(
371
+ 'Walk to the elder',
372
+ (client) => {
373
+ const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
374
+ if (hero) client.session.send(walk.make({ unit: hero.id, to: { x: ELDER_AT.x, z: ELDER_AT.z - 2 } }))
375
+ },
376
+ 'Walks the knight to the elder: she gives him the quest (the ! over her head), or takes it back done (the ?).'
377
+ ),
378
+ button(
379
+ 'Ride to the camp',
380
+ (client) => {
381
+ const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
382
+ if (hero) client.session.send(walk.make({ unit: hero.id, to: { x: 35.5, z: 30 } }))
383
+ },
384
+ 'Sends the knight to the bandits’ camp: he fights them as they come at him, and the quest counts each one he beats.'
385
+ ),
386
+ readout(
387
+ 'Quest',
388
+ (client) => {
389
+ const log = client.ui.objectives()
390
+ const open = log.find((each) => each.status === 'open')
391
+ if (!log.length) return 'none yet'
392
+ if (!open) return 'complete'
393
+ return open.progress !== undefined ? `${open.text} (${Math.round(open.progress * 3)}/3)` : open.text
394
+ },
395
+ 'The quest as the log has it: not yet given, open with how far along, or complete.'
396
+ )
397
+ ],
398
+ ready: (client) => {
399
+ aim(client, { x: 27, z: 18 }, 34)
400
+ const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
401
+ if (hero) client.controller.select([hero.id])
402
+ const mark = questMark(client)
403
+ const out = villageFx(client)
404
+ return () => {
405
+ mark()
406
+ out()
407
+ }
408
+ }
409
+ },
410
+ 'dialogs-barks': {
411
+ view: (view) => ({
412
+ ...view,
413
+ input: {
414
+ ...view.input,
415
+ // a unit clicked on answers; one ordered about acknowledges (what the client hears before it sends)
416
+ selectionChanged: (ids, client) => {
417
+ if (ids.length === 1) bubble(client, ids[0], next(CLICKED), 1.8)
418
+ },
419
+ beforeSend: (command, client) => {
420
+ if ((command.type === 'order' || command.type === 'smart') && 'entities' in command) {
421
+ const first = (command.entities as number[])[0]
422
+ if (first !== undefined) bubble(client, first, next(ORDERED), 1.6)
423
+ }
424
+ return command
425
+ }
426
+ }
427
+ }),
428
+ controls: [
429
+ button('Bark now', (client) => chatter(client), 'One of the villagers says a line now, in a bubble over their head.'),
430
+ button(
431
+ 'Wander',
432
+ (client) => client.session.send(wander.make({})),
433
+ 'The villagers walk about the square, talking as they go: the bubbles follow them.'
434
+ ),
435
+ toggle(
436
+ 'Chatter',
437
+ true,
438
+ (_client, on) => (barks.chatter = on),
439
+ 'Whether the villagers keep talking on their own, one every second or two; click one, or order it about, and it answers either way.'
440
+ ),
441
+ select(
442
+ 'Shown as',
443
+ ['bubble', 'floating words'],
444
+ 'bubble',
445
+ (_client, style) => (barks.style = style as typeof barks.style),
446
+ 'How a bark shows: a speech bubble pinned over the speaker, or words that float up off them and fade.'
447
+ )
448
+ ],
449
+ ready: (client) => {
450
+ aim(client, SQUARE, 20)
451
+ const out = villageFx(client)
452
+ const timer = setInterval(() => barks.chatter && chatter(client), 1700)
453
+ const walking = setInterval(() => client.session.send(wander.make({})), 9000)
454
+ return () => {
455
+ clearInterval(timer)
456
+ clearInterval(walking)
457
+ out()
458
+ }
459
+ }
460
+ },
461
+ 'dialogs-messages': {
462
+ controls: [
463
+ button(
464
+ 'Info',
465
+ (client) => client.session.send(news.make({ kind: 'info' })),
466
+ 'A plain message to the player in the message area, from the rules (world.message, "info").'
467
+ ),
468
+ button(
469
+ 'Hint',
470
+ (client) => client.session.send(news.make({ kind: 'hint' })),
471
+ 'A hint to the player, drawn apart from plain messages (world.message, "hint").'
472
+ ),
473
+ button(
474
+ 'Error',
475
+ (client) => client.session.send(news.make({ kind: 'error' })),
476
+ 'An error, in red, as a refused order says it (world.message).'
477
+ ),
478
+ button(
479
+ 'New objective',
480
+ (client) => client.session.send(news.make({ kind: 'objective' })),
481
+ 'Adds an objective to the quest log, top left, and announces it (world.objectives.add).'
482
+ ),
483
+ button(
484
+ 'Objective done',
485
+ (client) => client.session.send(news.make({ kind: 'done' })),
486
+ 'Marks the last objective done: it is ticked in the log and announced (world.objectives.complete).'
487
+ ),
488
+ button(
489
+ 'A line',
490
+ (client) => client.session.send(news.make({ kind: 'line' })),
491
+ 'The elder says a line in the dialog box, her face beside it (world.dialog.say).'
492
+ ),
493
+ button(
494
+ 'Alert',
495
+ (client) => {
496
+ const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
497
+ if (hero) client.alert(hero.id)
498
+ },
499
+ 'Raises an alert on the knight, as if he were under attack: a flash on the minimap, and Space jumps the camera there (client.alert).'
500
+ ),
501
+ button(
502
+ 'Ping',
503
+ (client) => {
504
+ client.ping(SQUARE.x + 4, SQUARE.z + 3)
505
+ client.minimapPing(SQUARE.x + 4, SQUARE.z + 3, '#ffd23c', 4)
506
+ },
507
+ 'Pings a spot of the square: a ring on the ground and a mark on the minimap.'
508
+ ),
509
+ button(
510
+ 'Floating words',
511
+ (client) => {
512
+ const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
513
+ if (hero) client.floats.show({ text: 'Level up!', style: { color: '#ffd23c', size: 22 }, seconds: 2 }, { on: hero.id })
514
+ },
515
+ '“Level up!” floats up off the knight and fades (client.floats.show).'
516
+ )
517
+ ],
518
+ ready: (client) => {
519
+ atTheFire(client)
520
+ return villageFx(client)
521
+ }
522
+ },
523
+ 'dialogs-own-box': {
524
+ view: (view) => ({
525
+ ...view,
526
+ cinematics: { ...view.cinematics, dialog: { mount: parchmentBox }, choice: { mount: sealChoice } }
527
+ }),
528
+ controls: [
529
+ button(
530
+ 'Talk',
531
+ (client) => void client.play(parchmentTalk, { letterbox: false, skippable: true }),
532
+ 'Plays the conversation in the game’s own parchment box instead of the client’s: click the scroll, or press Enter, to move on.'
533
+ )
534
+ ],
535
+ ready: (client) => {
536
+ atTheFire(client)
537
+ const out = villageFx(client)
538
+ const timer = setTimeout(() => void client.play(parchmentTalk, { letterbox: false }), 800)
539
+ return () => {
540
+ clearTimeout(timer)
541
+ out()
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }