@origingame/create-game 0.0.0-stage → 0.1.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 (581) hide show
  1. package/README.md +57 -2
  2. package/archetypes/README.md +99 -0
  3. package/archetypes/idle/.origin/setup +14 -0
  4. package/archetypes/idle/LICENSES.md +12 -0
  5. package/archetypes/idle/README.md +234 -0
  6. package/archetypes/idle/assets/audio/buy.wav +0 -0
  7. package/archetypes/idle/assets/audio/milestone.wav +0 -0
  8. package/archetypes/idle/assets/audio/nova.wav +0 -0
  9. package/archetypes/idle/assets/audio/orbit.wav +0 -0
  10. package/archetypes/idle/assets/audio/tap.wav +0 -0
  11. package/archetypes/idle/assets/audio/ui-close.ogt +9 -0
  12. package/archetypes/idle/assets/audio/ui-error.ogt +9 -0
  13. package/archetypes/idle/assets/audio/ui-open.ogt +9 -0
  14. package/archetypes/idle/assets/audio/ui-press.ogt +9 -0
  15. package/archetypes/idle/assets/audio/ui-success.ogt +9 -0
  16. package/archetypes/idle/assets/fonts/NotoSansSC-subset.ttf +0 -0
  17. package/archetypes/idle/assets/fonts/NotoSansSC-subset.ttf.ogt +10 -0
  18. package/archetypes/idle/assets/fonts/OFL.txt +93 -0
  19. package/archetypes/idle/assets/fx/drift.ogt +21 -0
  20. package/archetypes/idle/assets/fx/milestone.ogt +21 -0
  21. package/archetypes/idle/assets/fx/nova.ogt +22 -0
  22. package/archetypes/idle/assets/fx/stars.ogt +20 -0
  23. package/archetypes/idle/assets/fx/tap.ogt +23 -0
  24. package/archetypes/idle/assets/looks/idle.ogt +17 -0
  25. package/archetypes/idle/assets/skins/sparkorb/ChakraPetch-BoldItalic.ttf +0 -0
  26. package/archetypes/idle/assets/skins/sparkorb/ChakraPetch-BoldItalic.ttf.ogt +9 -0
  27. package/archetypes/idle/assets/skins/sparkorb/LICENSES.md +19 -0
  28. package/archetypes/idle/assets/skins/sparkorb/OFL.txt +95 -0
  29. package/archetypes/idle/assets/skins/sparkorb/Orbitron-Medium.ttf +0 -0
  30. package/archetypes/idle/assets/skins/sparkorb/Orbitron-Medium.ttf.ogt +9 -0
  31. package/archetypes/idle/assets/skins/sparkorb/Rajdhani-Bold.ttf +0 -0
  32. package/archetypes/idle/assets/skins/sparkorb/Rajdhani-Bold.ttf.ogt +9 -0
  33. package/archetypes/idle/assets/skins/sparkorb/atlas.png +0 -0
  34. package/archetypes/idle/assets/skins/sparkorb/atlas.png.ogt +7 -0
  35. package/archetypes/idle/assets/skins/sparkorb/icons.png +0 -0
  36. package/archetypes/idle/assets/skins/sparkorb/icons.png.ogt +7 -0
  37. package/archetypes/idle/assets/skins/sparkorb/skin.ogt +113 -0
  38. package/archetypes/idle/assets/sprites/glow.png +0 -0
  39. package/archetypes/idle/assets/sprites/glow.png.ogt +8 -0
  40. package/archetypes/idle/assets/sprites/mote-crystal.png +0 -0
  41. package/archetypes/idle/assets/sprites/mote-crystal.png.ogt +7 -0
  42. package/archetypes/idle/assets/sprites/mote-ember.png +0 -0
  43. package/archetypes/idle/assets/sprites/mote-ember.png.ogt +7 -0
  44. package/archetypes/idle/assets/sprites/mote-star.png +0 -0
  45. package/archetypes/idle/assets/sprites/mote-star.png.ogt +7 -0
  46. package/archetypes/idle/assets/sprites/mote-storm.png +0 -0
  47. package/archetypes/idle/assets/sprites/mote-storm.png.ogt +7 -0
  48. package/archetypes/idle/assets/sprites/orb.png +0 -0
  49. package/archetypes/idle/assets/sprites/orb.png.ogt +7 -0
  50. package/archetypes/idle/assets/sprites/spark.png +0 -0
  51. package/archetypes/idle/assets/sprites/spark.png.ogt +7 -0
  52. package/archetypes/idle/assets/strings/en.ogt +62 -0
  53. package/archetypes/idle/assets/strings/zh-CN.ogt +62 -0
  54. package/archetypes/idle/gitattributes +22 -0
  55. package/archetypes/idle/host.d.ts +46 -0
  56. package/archetypes/idle/package.json +18 -0
  57. package/archetypes/idle/project.ogt +14 -0
  58. package/archetypes/idle/references/metal/card.png +0 -0
  59. package/archetypes/idle/references/metal/orbit.png +0 -0
  60. package/archetypes/idle/references/metal/title.png +0 -0
  61. package/archetypes/idle/src/components.ts +139 -0
  62. package/archetypes/idle/src/main.ts +247 -0
  63. package/archetypes/idle/src/numbers.ts +126 -0
  64. package/archetypes/idle/src/systems.ts +340 -0
  65. package/archetypes/idle/src/ui.ts +412 -0
  66. package/archetypes/idle/tests/away.og.ts +42 -0
  67. package/archetypes/idle/tests/flow.og.ts +57 -0
  68. package/archetypes/idle/tests/frames.og.ts +33 -0
  69. package/archetypes/idle/tests/frames.ts +134 -0
  70. package/archetypes/idle/tests/lib.ts +74 -0
  71. package/archetypes/idle/tests/milestone.og.ts +32 -0
  72. package/archetypes/idle/tests/nova.og.ts +52 -0
  73. package/archetypes/idle/tests/offline.og.ts +60 -0
  74. package/archetypes/idle/tests/pad.og.ts +55 -0
  75. package/archetypes/idle/tests/pause.og.ts +68 -0
  76. package/archetypes/idle/tests/phone.og.ts +50 -0
  77. package/archetypes/idle/tests/replay.og.ts +28 -0
  78. package/archetypes/idle/tools/make-assets.py +448 -0
  79. package/archetypes/idle/tools/oglite.py +285 -0
  80. package/archetypes/idle/tsconfig.json +17 -0
  81. package/archetypes/match3/.origin/setup +14 -0
  82. package/archetypes/match3/LICENSES.md +7 -0
  83. package/archetypes/match3/README.md +205 -0
  84. package/archetypes/match3/assets/audio/cascade.wav +0 -0
  85. package/archetypes/match3/assets/audio/clear.wav +0 -0
  86. package/archetypes/match3/assets/audio/lost.wav +0 -0
  87. package/archetypes/match3/assets/audio/refused.wav +0 -0
  88. package/archetypes/match3/assets/audio/select.wav +0 -0
  89. package/archetypes/match3/assets/audio/swap.wav +0 -0
  90. package/archetypes/match3/assets/audio/won.wav +0 -0
  91. package/archetypes/match3/assets/fonts/OFL.txt +93 -0
  92. package/archetypes/match3/assets/fonts/ui.ttf +0 -0
  93. package/archetypes/match3/assets/fonts/ui.ttf.ogt +11 -0
  94. package/archetypes/match3/assets/scenes/main.ogt +24 -0
  95. package/archetypes/match3/assets/sprites/backdrop.png +0 -0
  96. package/archetypes/match3/assets/sprites/board.ogt +21 -0
  97. package/archetypes/match3/assets/sprites/cells.ogt +7 -0
  98. package/archetypes/match3/assets/sprites/cells.png +0 -0
  99. package/archetypes/match3/assets/sprites/cells.png.ogt +7 -0
  100. package/archetypes/match3/assets/sprites/gems.ogt +75 -0
  101. package/archetypes/match3/assets/sprites/gems.png +0 -0
  102. package/archetypes/match3/assets/sprites/panel.png +0 -0
  103. package/archetypes/match3/assets/sprites/ring.png +0 -0
  104. package/archetypes/match3/gitattributes +22 -0
  105. package/archetypes/match3/host.d.ts +46 -0
  106. package/archetypes/match3/package.json +18 -0
  107. package/archetypes/match3/project.ogt +18 -0
  108. package/archetypes/match3/references/metal/cascade.png +0 -0
  109. package/archetypes/match3/references/metal/clearing.png +0 -0
  110. package/archetypes/match3/references/metal/dealt.png +0 -0
  111. package/archetypes/match3/src/art.ts +54 -0
  112. package/archetypes/match3/src/board.ts +112 -0
  113. package/archetypes/match3/src/components.ts +95 -0
  114. package/archetypes/match3/src/layout.ts +25 -0
  115. package/archetypes/match3/src/main.ts +199 -0
  116. package/archetypes/match3/src/systems.ts +631 -0
  117. package/archetypes/match3/tests/drag.og.ts +31 -0
  118. package/archetypes/match3/tests/endings.og.ts +40 -0
  119. package/archetypes/match3/tests/frames.og.ts +49 -0
  120. package/archetypes/match3/tests/frames.ts +134 -0
  121. package/archetypes/match3/tests/hidpi.og.ts +41 -0
  122. package/archetypes/match3/tests/lib.ts +64 -0
  123. package/archetypes/match3/tests/replay.og.ts +28 -0
  124. package/archetypes/match3/tests/sounds.og.ts +50 -0
  125. package/archetypes/match3/tests/swap.og.ts +67 -0
  126. package/archetypes/match3/tools/make-assets.py +404 -0
  127. package/archetypes/match3/tools/oglite.py +285 -0
  128. package/archetypes/match3/tsconfig.json +17 -0
  129. package/archetypes/rhythm/.origin/setup +14 -0
  130. package/archetypes/rhythm/LICENSES.md +13 -0
  131. package/archetypes/rhythm/README.md +235 -0
  132. package/archetypes/rhythm/assets/audio/bass0.wav +0 -0
  133. package/archetypes/rhythm/assets/audio/bass1.wav +0 -0
  134. package/archetypes/rhythm/assets/audio/bass2.wav +0 -0
  135. package/archetypes/rhythm/assets/audio/bass3.wav +0 -0
  136. package/archetypes/rhythm/assets/audio/chords0.wav +0 -0
  137. package/archetypes/rhythm/assets/audio/chords1.wav +0 -0
  138. package/archetypes/rhythm/assets/audio/chords2.wav +0 -0
  139. package/archetypes/rhythm/assets/audio/chords3.wav +0 -0
  140. package/archetypes/rhythm/assets/audio/clear.wav +0 -0
  141. package/archetypes/rhythm/assets/audio/click.wav +0 -0
  142. package/archetypes/rhythm/assets/audio/drums.wav +0 -0
  143. package/archetypes/rhythm/assets/audio/fail.wav +0 -0
  144. package/archetypes/rhythm/assets/audio/leadA.wav +0 -0
  145. package/archetypes/rhythm/assets/audio/leadB.wav +0 -0
  146. package/archetypes/rhythm/assets/audio/miss.wav +0 -0
  147. package/archetypes/rhythm/assets/audio/tap0.wav +0 -0
  148. package/archetypes/rhythm/assets/audio/tap1.wav +0 -0
  149. package/archetypes/rhythm/assets/audio/tap2.wav +0 -0
  150. package/archetypes/rhythm/assets/audio/ui-close.ogt +9 -0
  151. package/archetypes/rhythm/assets/audio/ui-error.ogt +9 -0
  152. package/archetypes/rhythm/assets/audio/ui-open.ogt +9 -0
  153. package/archetypes/rhythm/assets/audio/ui-press.ogt +9 -0
  154. package/archetypes/rhythm/assets/audio/ui-success.ogt +9 -0
  155. package/archetypes/rhythm/assets/charts/neon-pulse.ogt +919 -0
  156. package/archetypes/rhythm/assets/fonts/NotoSansSC-subset.ttf +0 -0
  157. package/archetypes/rhythm/assets/fonts/NotoSansSC-subset.ttf.ogt +10 -0
  158. package/archetypes/rhythm/assets/fonts/OFL.txt +93 -0
  159. package/archetypes/rhythm/assets/fx/hit.ogt +22 -0
  160. package/archetypes/rhythm/assets/skins/lanebeat/ChakraPetch-Medium.ttf +0 -0
  161. package/archetypes/rhythm/assets/skins/lanebeat/ChakraPetch-Medium.ttf.ogt +9 -0
  162. package/archetypes/rhythm/assets/skins/lanebeat/JetBrainsMono-Regular.ttf +0 -0
  163. package/archetypes/rhythm/assets/skins/lanebeat/JetBrainsMono-Regular.ttf.ogt +9 -0
  164. package/archetypes/rhythm/assets/skins/lanebeat/LICENSES.md +17 -0
  165. package/archetypes/rhythm/assets/skins/lanebeat/OFL.txt +95 -0
  166. package/archetypes/rhythm/assets/skins/lanebeat/Orbitron-Bold.ttf +0 -0
  167. package/archetypes/rhythm/assets/skins/lanebeat/Orbitron-Bold.ttf.ogt +9 -0
  168. package/archetypes/rhythm/assets/skins/lanebeat/atlas.png +0 -0
  169. package/archetypes/rhythm/assets/skins/lanebeat/atlas.png.ogt +7 -0
  170. package/archetypes/rhythm/assets/skins/lanebeat/icons.png +0 -0
  171. package/archetypes/rhythm/assets/skins/lanebeat/icons.png.ogt +7 -0
  172. package/archetypes/rhythm/assets/skins/lanebeat/skin.ogt +110 -0
  173. package/archetypes/rhythm/assets/sprites/lane.png +0 -0
  174. package/archetypes/rhythm/assets/sprites/lane.png.ogt +7 -0
  175. package/archetypes/rhythm/assets/sprites/note.png +0 -0
  176. package/archetypes/rhythm/assets/sprites/note.png.ogt +7 -0
  177. package/archetypes/rhythm/assets/sprites/ring.png +0 -0
  178. package/archetypes/rhythm/assets/sprites/ring.png.ogt +7 -0
  179. package/archetypes/rhythm/assets/sprites/spark.png +0 -0
  180. package/archetypes/rhythm/assets/sprites/spark.png.ogt +7 -0
  181. package/archetypes/rhythm/assets/strings/en.ogt +61 -0
  182. package/archetypes/rhythm/assets/strings/zh-CN.ogt +61 -0
  183. package/archetypes/rhythm/gitattributes +22 -0
  184. package/archetypes/rhythm/host.d.ts +46 -0
  185. package/archetypes/rhythm/package.json +18 -0
  186. package/archetypes/rhythm/project.ogt +14 -0
  187. package/archetypes/rhythm/references/metal/highway.png +0 -0
  188. package/archetypes/rhythm/references/metal/results.png +0 -0
  189. package/archetypes/rhythm/references/metal/title.png +0 -0
  190. package/archetypes/rhythm/src/components.ts +147 -0
  191. package/archetypes/rhythm/src/main.ts +202 -0
  192. package/archetypes/rhythm/src/systems.ts +490 -0
  193. package/archetypes/rhythm/src/ui.ts +421 -0
  194. package/archetypes/rhythm/tests/band.og.ts +45 -0
  195. package/archetypes/rhythm/tests/calibrate.og.ts +49 -0
  196. package/archetypes/rhythm/tests/clear.og.ts +51 -0
  197. package/archetypes/rhythm/tests/fail.og.ts +40 -0
  198. package/archetypes/rhythm/tests/flow.og.ts +54 -0
  199. package/archetypes/rhythm/tests/frames.og.ts +35 -0
  200. package/archetypes/rhythm/tests/frames.ts +134 -0
  201. package/archetypes/rhythm/tests/lib.ts +123 -0
  202. package/archetypes/rhythm/tests/pad.og.ts +41 -0
  203. package/archetypes/rhythm/tests/pause.og.ts +77 -0
  204. package/archetypes/rhythm/tests/phone.og.ts +44 -0
  205. package/archetypes/rhythm/tests/replay.og.ts +27 -0
  206. package/archetypes/rhythm/tests/timing.og.ts +37 -0
  207. package/archetypes/rhythm/tools/make-assets.py +383 -0
  208. package/archetypes/rhythm/tools/oglite.py +285 -0
  209. package/archetypes/rhythm/tsconfig.json +17 -0
  210. package/archetypes/roguelike/.origin/setup +14 -0
  211. package/archetypes/roguelike/LICENSES.md +14 -0
  212. package/archetypes/roguelike/README.md +256 -0
  213. package/archetypes/roguelike/assets/ai/foe.ogt +28 -0
  214. package/archetypes/roguelike/assets/ai/guardian.ogt +26 -0
  215. package/archetypes/roguelike/assets/audio/bump.wav +0 -0
  216. package/archetypes/roguelike/assets/audio/drink.wav +0 -0
  217. package/archetypes/roguelike/assets/audio/fall.wav +0 -0
  218. package/archetypes/roguelike/assets/audio/hit.wav +0 -0
  219. package/archetypes/roguelike/assets/audio/hurt.wav +0 -0
  220. package/archetypes/roguelike/assets/audio/item.wav +0 -0
  221. package/archetypes/roguelike/assets/audio/kill.wav +0 -0
  222. package/archetypes/roguelike/assets/audio/pickup.wav +0 -0
  223. package/archetypes/roguelike/assets/audio/roar.wav +0 -0
  224. package/archetypes/roguelike/assets/audio/stairs.wav +0 -0
  225. package/archetypes/roguelike/assets/audio/step.ogt +9 -0
  226. package/archetypes/roguelike/assets/audio/telegraph.wav +0 -0
  227. package/archetypes/roguelike/assets/audio/tomb.wav +0 -0
  228. package/archetypes/roguelike/assets/audio/ui-close.ogt +9 -0
  229. package/archetypes/roguelike/assets/audio/ui-error.ogt +9 -0
  230. package/archetypes/roguelike/assets/audio/ui-open.ogt +9 -0
  231. package/archetypes/roguelike/assets/audio/ui-press.ogt +9 -0
  232. package/archetypes/roguelike/assets/audio/ui-success.ogt +9 -0
  233. package/archetypes/roguelike/assets/audio/won.wav +0 -0
  234. package/archetypes/roguelike/assets/fonts/NotoSansSC-subset.ttf +0 -0
  235. package/archetypes/roguelike/assets/fonts/NotoSansSC-subset.ttf.ogt +10 -0
  236. package/archetypes/roguelike/assets/fonts/OFL.txt +93 -0
  237. package/archetypes/roguelike/assets/maps/dive.ogt +106 -0
  238. package/archetypes/roguelike/assets/skins/lampdive/DotGothic16-Regular.ttf +0 -0
  239. package/archetypes/roguelike/assets/skins/lampdive/DotGothic16-Regular.ttf.ogt +9 -0
  240. package/archetypes/roguelike/assets/skins/lampdive/LICENSES.md +16 -0
  241. package/archetypes/roguelike/assets/skins/lampdive/OFL.txt +93 -0
  242. package/archetypes/roguelike/assets/skins/lampdive/atlas.png +0 -0
  243. package/archetypes/roguelike/assets/skins/lampdive/atlas.png.ogt +7 -0
  244. package/archetypes/roguelike/assets/skins/lampdive/icons.png +0 -0
  245. package/archetypes/roguelike/assets/skins/lampdive/icons.png.ogt +7 -0
  246. package/archetypes/roguelike/assets/skins/lampdive/skin.ogt +115 -0
  247. package/archetypes/roguelike/assets/sprites/dive.ogt +50 -0
  248. package/archetypes/roguelike/assets/sprites/dive.png +0 -0
  249. package/archetypes/roguelike/assets/sprites/dive.png.ogt +7 -0
  250. package/archetypes/roguelike/assets/sprites/glow.png +0 -0
  251. package/archetypes/roguelike/assets/sprites/glow.png.ogt +7 -0
  252. package/archetypes/roguelike/assets/strings/en.ogt +57 -0
  253. package/archetypes/roguelike/assets/strings/zh-CN.ogt +57 -0
  254. package/archetypes/roguelike/assets/tiles/tomb.ogt +31 -0
  255. package/archetypes/roguelike/assets/tiles/tomb.png +0 -0
  256. package/archetypes/roguelike/assets/tiles/tomb.png.ogt +7 -0
  257. package/archetypes/roguelike/gitattributes +22 -0
  258. package/archetypes/roguelike/host.d.ts +46 -0
  259. package/archetypes/roguelike/package.json +18 -0
  260. package/archetypes/roguelike/project.ogt +14 -0
  261. package/archetypes/roguelike/references/metal/dive.png +0 -0
  262. package/archetypes/roguelike/references/metal/over.png +0 -0
  263. package/archetypes/roguelike/references/metal/title.png +0 -0
  264. package/archetypes/roguelike/src/art.ts +23 -0
  265. package/archetypes/roguelike/src/components.ts +169 -0
  266. package/archetypes/roguelike/src/dive.ts +144 -0
  267. package/archetypes/roguelike/src/foes.ts +174 -0
  268. package/archetypes/roguelike/src/main.ts +258 -0
  269. package/archetypes/roguelike/src/systems.ts +620 -0
  270. package/archetypes/roguelike/src/ui.ts +350 -0
  271. package/archetypes/roguelike/tests/flow.og.ts +54 -0
  272. package/archetypes/roguelike/tests/foes.og.ts +45 -0
  273. package/archetypes/roguelike/tests/frames.og.ts +32 -0
  274. package/archetypes/roguelike/tests/frames.ts +134 -0
  275. package/archetypes/roguelike/tests/lib.ts +83 -0
  276. package/archetypes/roguelike/tests/lose.og.ts +44 -0
  277. package/archetypes/roguelike/tests/pad.og.ts +54 -0
  278. package/archetypes/roguelike/tests/pause.og.ts +59 -0
  279. package/archetypes/roguelike/tests/phone.og.ts +54 -0
  280. package/archetypes/roguelike/tests/replay.og.ts +24 -0
  281. package/archetypes/roguelike/tests/win.og.ts +60 -0
  282. package/archetypes/roguelike/tools/make-assets.py +518 -0
  283. package/archetypes/roguelike/tools/oglite.py +285 -0
  284. package/archetypes/roguelike/tsconfig.json +17 -0
  285. package/archetypes/runner/.origin/setup +14 -0
  286. package/archetypes/runner/LICENSES.md +10 -0
  287. package/archetypes/runner/README.md +225 -0
  288. package/archetypes/runner/assets/audio/coin.wav +0 -0
  289. package/archetypes/runner/assets/audio/crash.wav +0 -0
  290. package/archetypes/runner/assets/audio/hold.ogt +9 -0
  291. package/archetypes/runner/assets/audio/jump.wav +0 -0
  292. package/archetypes/runner/assets/audio/start.wav +0 -0
  293. package/archetypes/runner/assets/fonts/OFL.txt +93 -0
  294. package/archetypes/runner/assets/fonts/ui.ttf +0 -0
  295. package/archetypes/runner/assets/fonts/ui.ttf.ogt +11 -0
  296. package/archetypes/runner/assets/maps/far.ogt +25 -0
  297. package/archetypes/runner/assets/maps/near.ogt +25 -0
  298. package/archetypes/runner/assets/maps/roofs.ogt +19 -0
  299. package/archetypes/runner/assets/scenes/main.ogt +35 -0
  300. package/archetypes/runner/assets/sprites/coin.ogt +11 -0
  301. package/archetypes/runner/assets/sprites/coin.png +0 -0
  302. package/archetypes/runner/assets/sprites/coin.png.ogt +7 -0
  303. package/archetypes/runner/assets/sprites/courier.ogt +32 -0
  304. package/archetypes/runner/assets/sprites/courier.png +0 -0
  305. package/archetypes/runner/assets/sprites/courier.png.ogt +7 -0
  306. package/archetypes/runner/assets/sprites/props.ogt +21 -0
  307. package/archetypes/runner/assets/sprites/props.png +0 -0
  308. package/archetypes/runner/assets/sprites/props.png.ogt +7 -0
  309. package/archetypes/runner/assets/sprites/sky.png +0 -0
  310. package/archetypes/runner/assets/sprites/sky.png.ogt +7 -0
  311. package/archetypes/runner/assets/tiles/city.ogt +18 -0
  312. package/archetypes/runner/assets/tiles/city.png +0 -0
  313. package/archetypes/runner/assets/tiles/city.png.ogt +7 -0
  314. package/archetypes/runner/gitattributes +22 -0
  315. package/archetypes/runner/host.d.ts +46 -0
  316. package/archetypes/runner/package.json +18 -0
  317. package/archetypes/runner/project.ogt +18 -0
  318. package/archetypes/runner/references/metal/frames-coin.png +0 -0
  319. package/archetypes/runner/references/metal/frames-crash.png +0 -0
  320. package/archetypes/runner/references/metal/frames-title.png +0 -0
  321. package/archetypes/runner/src/art.ts +57 -0
  322. package/archetypes/runner/src/components.ts +94 -0
  323. package/archetypes/runner/src/main.ts +241 -0
  324. package/archetypes/runner/src/systems.ts +553 -0
  325. package/archetypes/runner/tests/coins.og.ts +30 -0
  326. package/archetypes/runner/tests/flow.og.ts +72 -0
  327. package/archetypes/runner/tests/frames.og.ts +37 -0
  328. package/archetypes/runner/tests/frames.ts +134 -0
  329. package/archetypes/runner/tests/jump.og.ts +44 -0
  330. package/archetypes/runner/tests/lib.ts +42 -0
  331. package/archetypes/runner/tests/records.og.ts +43 -0
  332. package/archetypes/runner/tests/replay.og.ts +36 -0
  333. package/archetypes/runner/tests/sounds.og.ts +43 -0
  334. package/archetypes/runner/tests/tune.og.ts +36 -0
  335. package/archetypes/runner/tools/make-assets.py +603 -0
  336. package/archetypes/runner/tools/oglite.py +285 -0
  337. package/archetypes/runner/tsconfig.json +17 -0
  338. package/archetypes/third-person/.origin/setup +14 -0
  339. package/archetypes/third-person/LICENSES.md +23 -0
  340. package/archetypes/third-person/README.md +249 -0
  341. package/archetypes/third-person/assets/ai/sentinel-east.ogt +69 -0
  342. package/archetypes/third-person/assets/ai/sentinel-north.ogt +69 -0
  343. package/archetypes/third-person/assets/ai/sentinel-west.ogt +69 -0
  344. package/archetypes/third-person/assets/anims/humanoid-clips.glb +0 -0
  345. package/archetypes/third-person/assets/audio/alarm.wav +0 -0
  346. package/archetypes/third-person/assets/audio/dawn.wav +0 -0
  347. package/archetypes/third-person/assets/audio/grab.wav +0 -0
  348. package/archetypes/third-person/assets/audio/ignite.wav +0 -0
  349. package/archetypes/third-person/assets/audio/jump.wav +0 -0
  350. package/archetypes/third-person/assets/audio/night.wav +0 -0
  351. package/archetypes/third-person/assets/audio/splash.wav +0 -0
  352. package/archetypes/third-person/assets/audio/step.ogt +9 -0
  353. package/archetypes/third-person/assets/audio/ui-close.ogt +9 -0
  354. package/archetypes/third-person/assets/audio/ui-error.ogt +9 -0
  355. package/archetypes/third-person/assets/audio/ui-open.ogt +9 -0
  356. package/archetypes/third-person/assets/audio/ui-press.ogt +9 -0
  357. package/archetypes/third-person/assets/audio/ui-success.ogt +9 -0
  358. package/archetypes/third-person/assets/audio/vale.wav +0 -0
  359. package/archetypes/third-person/assets/fonts/NotoSansSC-subset.ttf +0 -0
  360. package/archetypes/third-person/assets/fonts/NotoSansSC-subset.ttf.ogt +10 -0
  361. package/archetypes/third-person/assets/fonts/OFL.txt +93 -0
  362. package/archetypes/third-person/assets/fx/burst.ogt +25 -0
  363. package/archetypes/third-person/assets/fx/fire.ogt +24 -0
  364. package/archetypes/third-person/assets/fx/smoke.ogt +24 -0
  365. package/archetypes/third-person/assets/fx/splash.ogt +24 -0
  366. package/archetypes/third-person/assets/luts/teal-orange.cube +4917 -0
  367. package/archetypes/third-person/assets/models/stout.glb +0 -0
  368. package/archetypes/third-person/assets/models/strider.glb +0 -0
  369. package/archetypes/third-person/assets/models/strider.glb.ogt +9 -0
  370. package/archetypes/third-person/assets/nav/vale.ogt +43 -0
  371. package/archetypes/third-person/assets/rigs/strider.ogt +6 -0
  372. package/archetypes/third-person/assets/skins/ember/Cinzel-Bold.ttf +0 -0
  373. package/archetypes/third-person/assets/skins/ember/Cinzel-Bold.ttf.ogt +9 -0
  374. package/archetypes/third-person/assets/skins/ember/LICENSES.md +13 -0
  375. package/archetypes/third-person/assets/skins/ember/Newsreader-Medium.ttf +0 -0
  376. package/archetypes/third-person/assets/skins/ember/Newsreader-Medium.ttf.ogt +9 -0
  377. package/archetypes/third-person/assets/skins/ember/OFL.txt +94 -0
  378. package/archetypes/third-person/assets/skins/ember/atlas.png +0 -0
  379. package/archetypes/third-person/assets/skins/ember/atlas.png.ogt +7 -0
  380. package/archetypes/third-person/assets/skins/ember/icons.png +0 -0
  381. package/archetypes/third-person/assets/skins/ember/icons.png.ogt +7 -0
  382. package/archetypes/third-person/assets/skins/ember/skin.ogt +109 -0
  383. package/archetypes/third-person/assets/strings/en.ogt +47 -0
  384. package/archetypes/third-person/assets/strings/zh-CN.ogt +47 -0
  385. package/archetypes/third-person/assets/terrain/vale-control.png +0 -0
  386. package/archetypes/third-person/assets/terrain/vale.ogt +42 -0
  387. package/archetypes/third-person/assets/terrain/vale.png +0 -0
  388. package/archetypes/third-person/assets/textures/decals.png +0 -0
  389. package/archetypes/third-person/assets/textures/flame.png +0 -0
  390. package/archetypes/third-person/assets/textures/smoke.png +0 -0
  391. package/archetypes/third-person/assets/textures/spark.png +0 -0
  392. package/archetypes/third-person/assets/textures/terrain-grass.png +0 -0
  393. package/archetypes/third-person/assets/textures/terrain-path.png +0 -0
  394. package/archetypes/third-person/assets/textures/terrain-rock.png +0 -0
  395. package/archetypes/third-person/assets/textures/terrain-sand.png +0 -0
  396. package/archetypes/third-person/gitattributes +22 -0
  397. package/archetypes/third-person/host.d.ts +46 -0
  398. package/archetypes/third-person/package.json +18 -0
  399. package/archetypes/third-person/project.ogt +14 -0
  400. package/archetypes/third-person/references/metal/beacon.png +0 -0
  401. package/archetypes/third-person/references/metal/dawn.png +0 -0
  402. package/archetypes/third-person/references/metal/title.png +0 -0
  403. package/archetypes/third-person/references/webgl2/beacon.png +0 -0
  404. package/archetypes/third-person/src/anim.ts +87 -0
  405. package/archetypes/third-person/src/beacons.ts +80 -0
  406. package/archetypes/third-person/src/components.ts +148 -0
  407. package/archetypes/third-person/src/hero.ts +293 -0
  408. package/archetypes/third-person/src/main.ts +164 -0
  409. package/archetypes/third-person/src/sentinels.ts +159 -0
  410. package/archetypes/third-person/src/sound.ts +66 -0
  411. package/archetypes/third-person/src/ui.ts +451 -0
  412. package/archetypes/third-person/src/world.ts +212 -0
  413. package/archetypes/third-person/tests/flow.og.ts +70 -0
  414. package/archetypes/third-person/tests/frames.og.ts +35 -0
  415. package/archetypes/third-person/tests/frames.ts +134 -0
  416. package/archetypes/third-person/tests/hitstop.og.ts +26 -0
  417. package/archetypes/third-person/tests/lib.ts +87 -0
  418. package/archetypes/third-person/tests/lose.og.ts +44 -0
  419. package/archetypes/third-person/tests/pause.og.ts +64 -0
  420. package/archetypes/third-person/tests/phone.og.ts +65 -0
  421. package/archetypes/third-person/tests/replay.og.ts +32 -0
  422. package/archetypes/third-person/tests/restart.og.ts +28 -0
  423. package/archetypes/third-person/tests/sentinels.og.ts +48 -0
  424. package/archetypes/third-person/tests/win.og.ts +62 -0
  425. package/archetypes/third-person/tools/make-assets.py +353 -0
  426. package/archetypes/third-person/tools/oglite.py +285 -0
  427. package/archetypes/third-person/tsconfig.json +17 -0
  428. package/archetypes/topdown/.origin/setup +14 -0
  429. package/archetypes/topdown/LICENSES.md +10 -0
  430. package/archetypes/topdown/README.md +235 -0
  431. package/archetypes/topdown/assets/ai/slime.ogt +49 -0
  432. package/archetypes/topdown/assets/audio/coin.wav +0 -0
  433. package/archetypes/topdown/assets/audio/door.wav +0 -0
  434. package/archetypes/topdown/assets/audio/hit.wav +0 -0
  435. package/archetypes/topdown/assets/audio/hurt.wav +0 -0
  436. package/archetypes/topdown/assets/audio/key.wav +0 -0
  437. package/archetypes/topdown/assets/audio/step.ogt +9 -0
  438. package/archetypes/topdown/assets/fonts/OFL.txt +93 -0
  439. package/archetypes/topdown/assets/fonts/ui.ttf +0 -0
  440. package/archetypes/topdown/assets/fonts/ui.ttf.ogt +9 -0
  441. package/archetypes/topdown/assets/maps/tomb.tmj +2183 -0
  442. package/archetypes/topdown/assets/sprites/hero.ogt +58 -0
  443. package/archetypes/topdown/assets/sprites/hero.png +0 -0
  444. package/archetypes/topdown/assets/sprites/hero.png.ogt +7 -0
  445. package/archetypes/topdown/assets/sprites/items.ogt +15 -0
  446. package/archetypes/topdown/assets/sprites/items.png +0 -0
  447. package/archetypes/topdown/assets/sprites/items.png.ogt +7 -0
  448. package/archetypes/topdown/assets/sprites/slime.ogt +17 -0
  449. package/archetypes/topdown/assets/sprites/slime.png +0 -0
  450. package/archetypes/topdown/assets/sprites/slime.png.ogt +7 -0
  451. package/archetypes/topdown/assets/tiles/dungeon.ogt +65 -0
  452. package/archetypes/topdown/assets/tiles/dungeon.png +0 -0
  453. package/archetypes/topdown/assets/tiles/dungeon.png.ogt +7 -0
  454. package/archetypes/topdown/gitattributes +22 -0
  455. package/archetypes/topdown/host.d.ts +46 -0
  456. package/archetypes/topdown/package.json +18 -0
  457. package/archetypes/topdown/project.ogt +14 -0
  458. package/archetypes/topdown/references/metal/crit.png +0 -0
  459. package/archetypes/topdown/references/metal/dash.png +0 -0
  460. package/archetypes/topdown/references/metal/east.png +0 -0
  461. package/archetypes/topdown/references/metal/start.png +0 -0
  462. package/archetypes/topdown/src/art.ts +48 -0
  463. package/archetypes/topdown/src/components.ts +128 -0
  464. package/archetypes/topdown/src/main.ts +275 -0
  465. package/archetypes/topdown/src/systems.ts +503 -0
  466. package/archetypes/topdown/tests/ai.og.ts +64 -0
  467. package/archetypes/topdown/tests/combat.og.ts +72 -0
  468. package/archetypes/topdown/tests/door.og.ts +62 -0
  469. package/archetypes/topdown/tests/flee.og.ts +49 -0
  470. package/archetypes/topdown/tests/frames.og.ts +45 -0
  471. package/archetypes/topdown/tests/frames.ts +134 -0
  472. package/archetypes/topdown/tests/lib.ts +71 -0
  473. package/archetypes/topdown/tests/replay.og.ts +40 -0
  474. package/archetypes/topdown/tests/walk.og.ts +46 -0
  475. package/archetypes/topdown/tools/make-assets.py +623 -0
  476. package/archetypes/topdown/tools/oglite.py +285 -0
  477. package/archetypes/topdown/tsconfig.json +17 -0
  478. package/archetypes/tower-defense/.origin/setup +14 -0
  479. package/archetypes/tower-defense/LICENSES.md +15 -0
  480. package/archetypes/tower-defense/README.md +240 -0
  481. package/archetypes/tower-defense/assets/ai/plant.ogt +16 -0
  482. package/archetypes/tower-defense/assets/audio/clear.wav +0 -0
  483. package/archetypes/tower-defense/assets/audio/frost.wav +0 -0
  484. package/archetypes/tower-defense/assets/audio/leak.wav +0 -0
  485. package/archetypes/tower-defense/assets/audio/lost.wav +0 -0
  486. package/archetypes/tower-defense/assets/audio/night.wav +0 -0
  487. package/archetypes/tower-defense/assets/audio/plant.wav +0 -0
  488. package/archetypes/tower-defense/assets/audio/pop.wav +0 -0
  489. package/archetypes/tower-defense/assets/audio/thorn.wav +0 -0
  490. package/archetypes/tower-defense/assets/audio/ui-close.ogt +9 -0
  491. package/archetypes/tower-defense/assets/audio/ui-error.ogt +9 -0
  492. package/archetypes/tower-defense/assets/audio/ui-open.ogt +9 -0
  493. package/archetypes/tower-defense/assets/audio/ui-press.ogt +9 -0
  494. package/archetypes/tower-defense/assets/audio/ui-success.ogt +9 -0
  495. package/archetypes/tower-defense/assets/audio/wave.wav +0 -0
  496. package/archetypes/tower-defense/assets/audio/won.wav +0 -0
  497. package/archetypes/tower-defense/assets/fonts/NotoSansSC-subset.ttf +0 -0
  498. package/archetypes/tower-defense/assets/fonts/NotoSansSC-subset.ttf.ogt +10 -0
  499. package/archetypes/tower-defense/assets/fonts/OFL.txt +93 -0
  500. package/archetypes/tower-defense/assets/fx/fireflies.ogt +21 -0
  501. package/archetypes/tower-defense/assets/fx/pop.ogt +21 -0
  502. package/archetypes/tower-defense/assets/maps/garden.ogt +91 -0
  503. package/archetypes/tower-defense/assets/skins/pathguard/Fredoka-SemiBold.ttf +0 -0
  504. package/archetypes/tower-defense/assets/skins/pathguard/Fredoka-SemiBold.ttf.ogt +9 -0
  505. package/archetypes/tower-defense/assets/skins/pathguard/LICENSES.md +13 -0
  506. package/archetypes/tower-defense/assets/skins/pathguard/LilitaOne-Regular.ttf +0 -0
  507. package/archetypes/tower-defense/assets/skins/pathguard/LilitaOne-Regular.ttf.ogt +9 -0
  508. package/archetypes/tower-defense/assets/skins/pathguard/OFL.txt +94 -0
  509. package/archetypes/tower-defense/assets/skins/pathguard/atlas.png +0 -0
  510. package/archetypes/tower-defense/assets/skins/pathguard/atlas.png.ogt +7 -0
  511. package/archetypes/tower-defense/assets/skins/pathguard/icons.png +0 -0
  512. package/archetypes/tower-defense/assets/skins/pathguard/icons.png.ogt +7 -0
  513. package/archetypes/tower-defense/assets/skins/pathguard/skin.ogt +111 -0
  514. package/archetypes/tower-defense/assets/sprites/garden.ogt +66 -0
  515. package/archetypes/tower-defense/assets/sprites/garden.png +0 -0
  516. package/archetypes/tower-defense/assets/sprites/garden.png.ogt +7 -0
  517. package/archetypes/tower-defense/assets/sprites/glow.png +0 -0
  518. package/archetypes/tower-defense/assets/sprites/glow.png.ogt +7 -0
  519. package/archetypes/tower-defense/assets/sprites/ring.png +0 -0
  520. package/archetypes/tower-defense/assets/sprites/ring.png.ogt +7 -0
  521. package/archetypes/tower-defense/assets/sprites/spark.png +0 -0
  522. package/archetypes/tower-defense/assets/sprites/spark.png.ogt +7 -0
  523. package/archetypes/tower-defense/assets/strings/en.ogt +47 -0
  524. package/archetypes/tower-defense/assets/strings/zh-CN.ogt +47 -0
  525. package/archetypes/tower-defense/assets/tiles/garden.ogt +10 -0
  526. package/archetypes/tower-defense/assets/tiles/garden.png +0 -0
  527. package/archetypes/tower-defense/assets/tiles/garden.png.ogt +7 -0
  528. package/archetypes/tower-defense/gitattributes +22 -0
  529. package/archetypes/tower-defense/host.d.ts +46 -0
  530. package/archetypes/tower-defense/package.json +18 -0
  531. package/archetypes/tower-defense/project.ogt +14 -0
  532. package/archetypes/tower-defense/references/metal/end.png +0 -0
  533. package/archetypes/tower-defense/references/metal/title.png +0 -0
  534. package/archetypes/tower-defense/references/metal/wave.png +0 -0
  535. package/archetypes/tower-defense/src/art.ts +24 -0
  536. package/archetypes/tower-defense/src/components.ts +156 -0
  537. package/archetypes/tower-defense/src/main.ts +253 -0
  538. package/archetypes/tower-defense/src/systems.ts +658 -0
  539. package/archetypes/tower-defense/src/ui.ts +382 -0
  540. package/archetypes/tower-defense/tests/flow.og.ts +56 -0
  541. package/archetypes/tower-defense/tests/frames.og.ts +31 -0
  542. package/archetypes/tower-defense/tests/frames.ts +134 -0
  543. package/archetypes/tower-defense/tests/lib.ts +81 -0
  544. package/archetypes/tower-defense/tests/lose.og.ts +40 -0
  545. package/archetypes/tower-defense/tests/maze.og.ts +53 -0
  546. package/archetypes/tower-defense/tests/pad.og.ts +41 -0
  547. package/archetypes/tower-defense/tests/pause.og.ts +59 -0
  548. package/archetypes/tower-defense/tests/phone.og.ts +49 -0
  549. package/archetypes/tower-defense/tests/replay.og.ts +28 -0
  550. package/archetypes/tower-defense/tests/win.og.ts +58 -0
  551. package/archetypes/tower-defense/tools/make-assets.py +640 -0
  552. package/archetypes/tower-defense/tools/oglite.py +285 -0
  553. package/archetypes/tower-defense/tsconfig.json +17 -0
  554. package/bin/create.ts +496 -0
  555. package/looks/catalogue.json +1871 -0
  556. package/package.json +31 -4
  557. package/template/.origin/setup +14 -0
  558. package/template/LICENSES.md +23 -0
  559. package/template/README.md +37 -0
  560. package/template/assets/ai/guard.ogt +48 -0
  561. package/template/assets/audio/blip.wav +0 -0
  562. package/template/assets/audio/click.ogt +9 -0
  563. package/template/assets/fonts/NotoSansSC-Regular-subset.ttf +0 -0
  564. package/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
  565. package/template/assets/fonts/OFL.txt +93 -0
  566. package/template/assets/looks/origin-game.ogt +12 -0
  567. package/template/assets/models/dropship.glb +0 -0
  568. package/template/assets/models/walker.glb +0 -0
  569. package/template/assets/nav/yard.ogt +27 -0
  570. package/template/assets/scenes/main.ogt +34 -0
  571. package/template/assets/strings/en.ogt +14 -0
  572. package/template/assets/textures/stone.png +0 -0
  573. package/template/assets/textures/stone_n.png +0 -0
  574. package/template/assets/textures/stone_n.png.ogt +7 -0
  575. package/template/gitattributes +22 -0
  576. package/template/package.json +18 -0
  577. package/template/project.ogt +25 -0
  578. package/template/src/main.ts +246 -0
  579. package/template/tests/guard.og.ts +67 -0
  580. package/template/tests/smoke.og.ts +43 -0
  581. package/template/tsconfig.json +17 -0
@@ -0,0 +1,503 @@
1
+ // Tomb Room's systems, in tick order: Control (the hero: walk, swing, dash,
2
+ // hurt), Slimes (take hits, knockback and death; what a slime decides is
3
+ // assets/ai/slime.ogt, run by og.ai before these systems and moved by og.nav
4
+ // after them), Pickups (coins and the key), Door (the locked doorway and the
5
+ // rooms), Sfx (the voices) and Hud (screen text). `biteHero` is the
6
+ // behaviour's `bite` command.
7
+ //
8
+ // Control reads the hero's flipbook events: a walk clip's "step" counts a
9
+ // footstep, an attack clip's "hit" is the sword's strike frame, so the
10
+ // damage lands on the frame the blade is out. Every hit is felt five ways:
11
+ // the slime flashes white and squashes, it is knocked back, the world holds
12
+ // for a few ticks (og.time.hitStop), the camera shakes and kicks, and the
13
+ // damage floats up (the third hit is a crit).
14
+
15
+ import { og, defineSystem } from "@origingame/engine";
16
+ import type { Entity, Value, Vector3, View, World } from "@origingame/engine";
17
+ import { HeroClip, heroClip } from "./art.ts";
18
+ import { Facing, Game, Hero, Pickup, PickupKind, Report, Slime, Sfx, Sound, Tuning } from "./components.ts";
19
+
20
+ /** The swing's length: the attack clips' two frames (0.07 s + 0.18 s). */
21
+ export const ATTACK_TICKS = 15;
22
+ /** Ticks after hp reaches 0 before the hero stands up at the spawn. */
23
+ const DOWN_TICKS = 90;
24
+ /** Damage per hit, in order; later hits repeat the last. */
25
+ const DAMAGE = [12, 12, 26] as const;
26
+ /** A struck slime's squash. */
27
+ const IMPACT = { x: 1.45, y: 0.6 } as const;
28
+ /** The chest: the sword and the pickups measure from this high above the feet. */
29
+ const CHEST = 0.45;
30
+
31
+ const WHITE = { r: 1, g: 1, b: 1 };
32
+ const GOLD = { r: 1, g: 0.72, b: 0.2 };
33
+ const RED = { r: 1, g: 0.25, b: 0.2 };
34
+ const MINT = { r: 0.55, g: 0.95, b: 0.7 };
35
+
36
+ function must(r: { ok: true } | { ok: false; reason: string }): void {
37
+ if (!r.ok) throw new Error(r.reason);
38
+ }
39
+
40
+ function fontOf(g: { font: number }): never {
41
+ return g.font as unknown as never;
42
+ }
43
+
44
+ /** A triangle wave of the tick, 0..1 over `period` ticks (tick arithmetic only, so every host agrees). */
45
+ function tri(tick: number, period: number): number {
46
+ const p = (tick % period) / period;
47
+ return p < 0.5 ? p * 2 : 2 - p * 2;
48
+ }
49
+
50
+ function float(world: World, font: never, text: string, at: Vector3, color: { r: number; g: number; b: number }, crit = false): void {
51
+ must(og.render.floatingText(world, { text, position: at, font, size: 0.42, color, crit, rise: 1.1, lifetime: 0.75 }));
52
+ }
53
+
54
+ /** The hero: input moves it on the tick it is read; Space swings, Shift dashes; hurt knocks it back and flickers it. */
55
+ export const Control = defineSystem(
56
+ "Control",
57
+ {
58
+ reads: [og.Input, Tuning, og.FlipbookEvent],
59
+ writes: [Game, Hero, og.Transform, og.CharacterController, og.Flipbook, og.Sprite, og.Afterimage],
60
+ },
61
+ (q, time, world) => {
62
+ const game = world.resource(Game);
63
+ const tune = world.resource(Tuning);
64
+ const input = world.resource(og.Input);
65
+ if (!game.ok || !tune.ok || !input.ok) return;
66
+ const g = game.value;
67
+ const t = tune.value;
68
+ const inp = input.value;
69
+ g.swing = false;
70
+ for (const a of q.archetypes()) {
71
+ const hero = a.column(Hero);
72
+ const p = a.column(og.Transform).position;
73
+ const v = a.column(og.CharacterController).move;
74
+ const fb = a.column(og.Flipbook);
75
+ const sprite = a.column(og.Sprite);
76
+ const ghost = a.column(og.Afterimage);
77
+ for (let i = 0; i < a.count; i++) {
78
+ const me = a.entity(i);
79
+ const x = p[i * 3]!;
80
+ const y = p[i * 3 + 1]!;
81
+ g.heroX = x;
82
+ g.heroY = y;
83
+ // The flipbook's frame events from the hero's clips.
84
+ for (const e of world.events(og.FlipbookEvent)) {
85
+ if (e.entity !== me) continue;
86
+ if (e.name === "step") {
87
+ g.steps += 1;
88
+ g.sfx |= Sound.Step;
89
+ }
90
+ if (e.name === "hit") {
91
+ const d = facingVector(g.facing);
92
+ g.swing = true;
93
+ g.swings += 1;
94
+ g.swingX = x + d.x * t.reach;
95
+ g.swingY = y + CHEST + d.y * t.reach;
96
+ }
97
+ }
98
+ // A hit stop holds the body (physics is frozen); the timers wait with it.
99
+ if (time.hitStop > 0) continue;
100
+
101
+ if (g.iframes > 0) g.iframes -= 1;
102
+ sprite.opacity[i] = g.iframes > 0 && Math.floor(g.iframes / 4) % 2 === 1 ? 0.35 : 1;
103
+ g.knockX *= 0.8;
104
+ g.knockY *= 0.8;
105
+ if (Math.abs(g.knockX) < 0.05) g.knockX = 0;
106
+ if (Math.abs(g.knockY) < 0.05) g.knockY = 0;
107
+
108
+ // Down: the hero lies still, then stands up at the spawn with full hearts.
109
+ if (g.hp === 0) {
110
+ hero.attack[i] = 0;
111
+ hero.dash[i] = 0;
112
+ ghost.active[i] = 0;
113
+ v[i * 3] = 0;
114
+ v[i * 3 + 1] = 0;
115
+ g.downTicks += 1;
116
+ if (g.downTicks >= DOWN_TICKS) {
117
+ g.hp = g.hpMax;
118
+ g.downTicks = 0;
119
+ g.iframes = t.iframes;
120
+ p[i * 3] = g.spawnX;
121
+ p[i * 3 + 1] = g.spawnY;
122
+ }
123
+ continue;
124
+ }
125
+
126
+ // Which way the keys point: WASD and the arrows, summed and clamped.
127
+ const k1 = inp.axis2("KeyA", "KeyD", "KeyS", "KeyW");
128
+ const k2 = inp.axis2("ArrowLeft", "ArrowRight", "ArrowDown", "ArrowUp");
129
+ let mx = Math.max(-1, Math.min(1, k1.x + k2.x));
130
+ let my = Math.max(-1, Math.min(1, k1.y + k2.y));
131
+ const len = Math.hypot(mx, my);
132
+ if (len > 0) {
133
+ mx /= len;
134
+ my /= len;
135
+ }
136
+ const moving = len > 0;
137
+ hero.moving[i] = moving ? 1 : 0;
138
+
139
+ if (hero.dashCooldown[i]! > 0) hero.dashCooldown[i] = hero.dashCooldown[i]! - 1;
140
+ const attackPressed = inp.pressed("Space") || inp.pressed("KeyJ");
141
+ const dashPressed = inp.pressed("ShiftLeft") || inp.pressed("ShiftRight") || inp.pressed("KeyK");
142
+
143
+ // Facing follows the stronger axis; a diagonal keeps the old facing when it is one of its two keys.
144
+ if (moving && hero.attack[i] === 0) g.facing = faceOf(mx, my, g.facing);
145
+
146
+ if (attackPressed && hero.attack[i] === 0 && hero.dash[i] === 0) hero.attack[i] = ATTACK_TICKS;
147
+ if (dashPressed && hero.attack[i] === 0 && hero.dash[i] === 0 && hero.dashCooldown[i] === 0) {
148
+ const d = moving ? { x: mx, y: my } : facingVector(g.facing);
149
+ hero.dash[i] = t.dashTicks;
150
+ hero.dashCooldown[i] = t.dashCooldown;
151
+ hero.dashX[i] = d.x;
152
+ hero.dashY[i] = d.y;
153
+ hero.dashes[i] = hero.dashes[i]! + 1;
154
+ }
155
+
156
+ let vx = 0;
157
+ let vy = 0;
158
+ if (hero.dash[i]! > 0) {
159
+ hero.dash[i] = hero.dash[i]! - 1;
160
+ vx = hero.dashX[i]! * t.dashSpeed;
161
+ vy = hero.dashY[i]! * t.dashSpeed;
162
+ } else if (hero.attack[i]! > 0) {
163
+ // Rooted while swinging: the blade, not the feet, carries the hit.
164
+ hero.attack[i] = hero.attack[i]! - 1;
165
+ } else if (moving) {
166
+ vx = mx * t.speed;
167
+ vy = my * t.speed;
168
+ }
169
+ ghost.active[i] = hero.dash[i]! > 0 ? 1 : 0;
170
+ v[i * 3] = vx + g.knockX;
171
+ v[i * 3 + 1] = vy + g.knockY;
172
+ v[i * 3 + 2] = 0;
173
+
174
+ // The clip: a swing, a walk or a stand, in the facing's row. A clip change restarts it.
175
+ const swinging = hero.attack[i]! > 0;
176
+ const action = swinging ? HeroClip.Attack : moving || hero.dash[i]! > 0 ? HeroClip.Walk : HeroClip.Idle;
177
+ const want = heroClip(action, g.facing);
178
+ if (fb.clip[i] !== want) fb.clip[i] = want;
179
+ sprite.flipX[i] = g.facing === Facing.Left ? 1 : 0;
180
+ }
181
+ }
182
+ },
183
+ );
184
+
185
+ export function facingVector(f: number): { x: number; y: number } {
186
+ return f === Facing.Up ? { x: 0, y: 1 } : f === Facing.Down ? { x: 0, y: -1 } : f === Facing.Left ? { x: -1, y: 0 } : { x: 1, y: 0 };
187
+ }
188
+
189
+ function faceOf(x: number, y: number, old: number): number {
190
+ const keeps = (old === Facing.Left && x < 0) || (old === Facing.Right && x > 0) || (old === Facing.Up && y > 0) || (old === Facing.Down && y < 0);
191
+ if (Math.abs(x) > 0.01 && Math.abs(y) > 0.01 && keeps) return old;
192
+ if (Math.abs(x) >= Math.abs(y)) return x < 0 ? Facing.Left : Facing.Right;
193
+ return y < 0 ? Facing.Down : Facing.Up;
194
+ }
195
+
196
+ /**
197
+ * Slimes take the sword's hits, are knocked back and stunned, and die. The
198
+ * walking is og.nav's (the agent's velocity) except while stunned, held by a
199
+ * hit stop or dead, when `NavAgent.drive` is off and this system owns the
200
+ * velocity; dead slimes stop thinking. Perception's sight follows the tunable.
201
+ */
202
+ export const Slimes = defineSystem(
203
+ "Slimes",
204
+ { reads: [Tuning], writes: [Game, Slime, og.Transform, og.Velocity, og.Flipbook, og.Sprite, og.NavAgent, og.AiAgent, og.Perception], after: ["Control"] },
205
+ (q, time, world) => {
206
+ const game = world.resource(Game);
207
+ const tune = world.resource(Tuning);
208
+ if (!game.ok || !tune.ok) return;
209
+ const g = game.value;
210
+ const t = tune.value;
211
+ const font = fontOf(g);
212
+ for (const a of q.archetypes()) {
213
+ const s = a.column(Slime);
214
+ const p = a.column(og.Transform).position;
215
+ const v = a.column(og.Velocity).linear;
216
+ const fb = a.column(og.Flipbook);
217
+ const sprite = a.column(og.Sprite);
218
+ const nav = a.column(og.NavAgent);
219
+ const ai = a.column(og.AiAgent);
220
+ const eyes = a.column(og.Perception);
221
+ for (let i = 0; i < a.count; i++) {
222
+ const me = a.entity(i);
223
+ const x = p[i * 3]!;
224
+ const y = p[i * 3 + 1]!;
225
+ eyes.sight[i] = g.hp > 0 ? t.slimeSight : 0;
226
+ if (s.dead[i]) {
227
+ nav.drive[i] = 0;
228
+ ai.active[i] = 0;
229
+ v[i * 3] = 0;
230
+ v[i * 3 + 1] = 0;
231
+ if (time.hitStop > 0) continue;
232
+ s.deadTicks[i] = s.deadTicks[i]! + 1;
233
+ if (s.deadTicks[i] === 30) sprite.visible[i] = 0;
234
+ continue;
235
+ }
236
+
237
+ // The sword's strike frame: a circle of radius reach around the swing point.
238
+ if (g.swing && Math.hypot(g.swingX - x, g.swingY - (y + 0.3)) < t.reach + 0.35) {
239
+ hitSlime(world, me, g, t, s, i, x, y, font);
240
+ fb.clip[i] = s.hurt[i]!;
241
+ }
242
+ if (s.dead[i] || time.hitStop > 0) {
243
+ nav.drive[i] = 0;
244
+ v[i * 3] = 0;
245
+ v[i * 3 + 1] = 0;
246
+ continue;
247
+ }
248
+ if (s.stun[i]! > 0) {
249
+ nav.drive[i] = 0;
250
+ s.stun[i] = s.stun[i]! - 1;
251
+ v[i * 3] = s.knockX[i]!;
252
+ v[i * 3 + 1] = s.knockY[i]!;
253
+ v[i * 3 + 2] = 0;
254
+ s.knockX[i] = s.knockX[i]! * 0.82;
255
+ s.knockY[i] = s.knockY[i]! * 0.82;
256
+ if (s.stun[i] === 0) fb.clip[i] = s.bob[i]!;
257
+ continue;
258
+ }
259
+ nav.drive[i] = 1;
260
+ }
261
+ }
262
+ },
263
+ );
264
+
265
+ /**
266
+ * `bite:<slime>`: the behaviour's attack (og.ai queues it with the slime's
267
+ * handle when the hero is in reach; it runs before the next tick). The hero
268
+ * is hurt and knocked away unless it is down or still blinking, and the
269
+ * slime bounces back off the bite, so it does not sit on the hero.
270
+ */
271
+ export function biteHero(world: World, args: readonly string[]): void {
272
+ const me = Number(args[0]) as Entity;
273
+ const game = world.resource(Game);
274
+ const tune = world.resource(Tuning);
275
+ const slime = world.get(me, Slime);
276
+ const at = world.get(me, og.Transform);
277
+ if (!game.ok || !tune.ok || !slime.ok || !at.ok) throw new Error(`bite: ${args[0]} is not a slime`);
278
+ const g = game.value;
279
+ const s = slime.value;
280
+ if (s.dead || s.stun > 0 || g.hp === 0 || g.iframes > 0) return;
281
+ const dx = g.heroX - at.value.position.x;
282
+ const dy = g.heroY - at.value.position.y;
283
+ const d = Math.hypot(dx, dy);
284
+ if (d > 0.85) return;
285
+ const ux = d > 0.01 ? dx / d : 0;
286
+ const uy = d > 0.01 ? dy / d : 1;
287
+ hurtHero(world, g, tune.value, ux, uy, fontOf(g));
288
+ must(world.set(me, Slime, { knockX: -ux * tune.value.knockback * 0.5, knockY: -uy * tune.value.knockback * 0.5, stun: 20, bites: s.bites + 1 }));
289
+ }
290
+
291
+ type GameValue = Value<typeof Game>;
292
+ type TuneValue = Value<typeof Tuning>;
293
+ type SlimeView = View<typeof Slime>;
294
+
295
+ function hitSlime(world: World, me: Entity, g: GameValue, t: TuneValue, s: SlimeView, i: number, x: number, y: number, font: never): void {
296
+ g.enemyHits += 1;
297
+ const dmg = DAMAGE[Math.min(g.enemyHits, DAMAGE.length) - 1]!;
298
+ const crit = g.enemyHits >= DAMAGE.length;
299
+ const hp = Math.max(0, s.hp[i]! - dmg);
300
+ s.hp[i] = hp;
301
+ g.enemyHp = hp;
302
+ // Knocked away from the hero.
303
+ let kx = x - g.heroX;
304
+ let ky = y - g.heroY;
305
+ const kd = Math.hypot(kx, ky);
306
+ if (kd > 0.01) {
307
+ kx /= kd;
308
+ ky /= kd;
309
+ } else {
310
+ const f = facingVector(g.facing);
311
+ kx = f.x;
312
+ ky = f.y;
313
+ }
314
+ s.knockX[i] = kx * t.knockback;
315
+ s.knockY[i] = ky * t.knockback;
316
+ s.stun[i] = 16;
317
+ must(og.tween.flash(world, { entity: me, duration: 0.14 }));
318
+ must(og.tween.squash(world, { entity: me, impact: IMPACT, duration: 0.28 }));
319
+ must(og.time.hitStop(world, crit ? t.hitStop + 3 : t.hitStop));
320
+ must(og.camera.shake(world, Math.min(1, crit ? t.shake * 1.8 : t.shake)));
321
+ must(og.camera.kick(world, { x: kx, y: ky, z: 0 }, crit ? 0.25 : 0.12));
322
+ float(world, font, crit ? `${dmg}!` : `${dmg}`, { x, y: y + 1.1, z: 0 }, crit ? GOLD : WHITE, crit);
323
+ g.sfx |= Sound.Hit;
324
+ if (hp === 0) {
325
+ s.dead[i] = 1;
326
+ s.deadTicks[i] = 0;
327
+ // The death: a last stretch upwards and a fade to nothing.
328
+ must(og.tween.to(world, { entity: me, field: "scale", to: [1.7, 0, 1], duration: 0.4, delay: 0.1, ease: "backIn" }));
329
+ must(og.tween.to(world, { entity: me, component: og.Sprite, field: "opacity", to: [0], duration: 0.4, delay: 0.1, ease: "quadIn" }));
330
+ }
331
+ }
332
+
333
+ function hurtHero(world: World, g: GameValue, t: TuneValue, dx: number, dy: number, font: never): void {
334
+ g.hp = Math.max(0, g.hp - 1);
335
+ g.hurts += 1;
336
+ g.iframes = t.iframes;
337
+ g.knockX = dx * t.knockback;
338
+ g.knockY = dy * t.knockback;
339
+ if (g.hp === 0) g.downTicks = 0;
340
+ const hero = world.find("hero");
341
+ if (hero !== null) {
342
+ must(og.tween.flash(world, { entity: hero, color: RED, duration: 0.2 }));
343
+ must(og.tween.squash(world, { entity: hero, impact: { x: 1.3, y: 0.75 }, duration: 0.22 }));
344
+ }
345
+ // A hit taken holds the world a light hit's length (og-feel §3), shorter
346
+ // than the hero's own blows, with the flash, the shake and the sound.
347
+ must(og.time.hitStop(world, Math.max(2, t.hitStop - 1)));
348
+ must(og.camera.shake(world, 0.4));
349
+ float(world, font, "-1", { x: g.heroX, y: g.heroY + 1.2, z: 0 }, RED);
350
+ g.sfx |= Sound.Hurt;
351
+ }
352
+
353
+ /** Coins and the key hover until the hero's chest touches them, then pop up and fade. */
354
+ export const Pickups = defineSystem(
355
+ "Pickups",
356
+ { writes: [Game, Pickup, og.Transform, og.Sprite], after: ["Slimes"] },
357
+ (q, time, world) => {
358
+ const game = world.resource(Game);
359
+ if (!game.ok) return;
360
+ const g = game.value;
361
+ if (time.hitStop > 0) return;
362
+ const font = fontOf(g);
363
+ for (const a of q.archetypes()) {
364
+ const pk = a.column(Pickup);
365
+ const p = a.column(og.Transform).position;
366
+ const sprite = a.column(og.Sprite);
367
+ for (let i = 0; i < a.count; i++) {
368
+ if (pk.taken[i]) {
369
+ pk.takenTicks[i] = pk.takenTicks[i]! + 1;
370
+ if (pk.takenTicks[i] === 24) sprite.visible[i] = 0;
371
+ continue;
372
+ }
373
+ const me = a.entity(i);
374
+ const x = p[i * 3]!;
375
+ const base = pk.baseY[i]!;
376
+ p[i * 3 + 1] = base + 0.08 * tri(world.tick + i * 13, 70);
377
+ if (g.hp === 0 || Math.hypot(g.heroX - x, g.heroY + CHEST - (base + 0.4)) > 0.7) continue;
378
+ pk.taken[i] = 1;
379
+ pk.takenTicks[i] = 0;
380
+ const at = { x, y: base + 1.1, z: 0 };
381
+ if (pk.kind[i] === PickupKind.Key) {
382
+ g.hasKey = true;
383
+ g.sfx |= Sound.Key;
384
+ float(world, font, "key!", at, GOLD, true);
385
+ } else {
386
+ g.coins += 1;
387
+ g.sfx |= Sound.Coin;
388
+ float(world, font, "+1", at, GOLD);
389
+ }
390
+ must(og.tween.to(world, { entity: me, field: "position.y", to: [base + 0.9], duration: 0.3, ease: "quadOut" }));
391
+ must(og.tween.to(world, { entity: me, component: og.Sprite, field: "opacity", to: [0], duration: 0.3, delay: 0.08, ease: "quadIn" }));
392
+ must(og.tween.squash(world, { entity: me, impact: { x: 0.6, y: 1.6 }, duration: 0.3 }));
393
+ const hero = world.find("hero");
394
+ if (hero !== null) must(og.tween.squash(world, { entity: hero, impact: { x: 0.85, y: 1.2 }, duration: 0.2 }));
395
+ }
396
+ }
397
+ },
398
+ );
399
+
400
+ /** The doorway: locked until the key; with it, the wall cells clear (re-tiling their neighbours and dropping their colliders). */
401
+ export const Door = defineSystem("Door", { writes: [Game], after: ["Pickups"] }, (_q, time, world) => {
402
+ const game = world.resource(Game);
403
+ if (!game.ok) return;
404
+ const g = game.value;
405
+ g.room = g.heroX < g.roomSplit ? 1 : 2;
406
+ if (time.hitStop > 0) return;
407
+ if (g.lockedFor > 0) g.lockedFor -= 1;
408
+ if (g.doorOpen || g.hp === 0) return;
409
+ const near = Math.abs(g.heroX - g.doorX) < 1.6 && Math.abs(g.heroY - g.doorY) < 1.3;
410
+ if (!near) return;
411
+ const font = fontOf(g);
412
+ if (!g.hasKey) {
413
+ if (g.lockedFor === 0) {
414
+ float(world, font, "locked", { x: g.doorX, y: g.doorY + 1.4, z: 0 }, WHITE);
415
+ g.lockedFor = 90;
416
+ }
417
+ return;
418
+ }
419
+ openDoor(world, g);
420
+ float(world, font, "open!", { x: g.doorX, y: g.doorY + 1.4, z: 0 }, MINT, true);
421
+ must(og.camera.shake(world, 0.3));
422
+ });
423
+
424
+ /** Clears the doorway's wall and door cells. Also the `give:door` command's work. */
425
+ export function openDoor(world: World, g: GameValue): void {
426
+ const map = world.find("tomb");
427
+ if (map === null) throw new Error("no entity named tomb");
428
+ for (let y = g.doorCellY; y < g.doorCellY + g.doorCellsH; y++) {
429
+ for (let x = g.doorCellX; x < g.doorCellX + g.doorCellsW; x++) {
430
+ must(og.tilemap.setTile(world, { entity: map, layer: "walls", x, y, tile: 0 }));
431
+ must(og.tilemap.setTile(world, { entity: map, layer: "door", x, y, tile: -1 }));
432
+ }
433
+ }
434
+ g.doorOpen = true;
435
+ g.hasKey = false;
436
+ g.sfx |= Sound.Door;
437
+ }
438
+
439
+ /** Plays each requested sound once: a voice already playing is stopped this tick and restarted the next. */
440
+ export const Voices = defineSystem("Voices", { reads: [Sfx], writes: [Game, og.Rng, og.AudioSource], after: ["Door"] }, (q, _time, world) => {
441
+ const game = world.resource(Game);
442
+ if (!game.ok) return;
443
+ const g = game.value;
444
+ if (g.sfx === 0) return;
445
+ const rng = world.resource(og.Rng);
446
+ let left = g.sfx;
447
+ for (const a of q.archetypes()) {
448
+ const bit = a.column(Sfx).bit;
449
+ const src = a.column(og.AudioSource);
450
+ for (let i = 0; i < a.count; i++) {
451
+ const b = bit[i]!;
452
+ if ((g.sfx & b) === 0) continue;
453
+ if (src.playing[i]) {
454
+ src.playing[i] = 0;
455
+ } else {
456
+ src.playing[i] = 1;
457
+ // Footsteps vary a little, so a walk does not machine-gun (og-audio §1).
458
+ if (b === Sound.Step && rng.ok) src.pitch[i] = rng.value.range(0.92, 1.08);
459
+ left &= ~b;
460
+ }
461
+ }
462
+ }
463
+ g.sfx = left;
464
+ });
465
+
466
+ /** The HUD: hearts, coins, the key and the room, in screen space; the controls for the first seconds. Also copies the facts into the hero's Report. */
467
+ export const Hud = defineSystem("Hud", { reads: [Game], writes: [Report], after: ["Voices"] }, (q, _time, world) => {
468
+ const game = world.resource(Game);
469
+ if (!game.ok) return;
470
+ const g = game.value;
471
+ const map = world.find("tomb");
472
+ for (const a of q.archetypes()) {
473
+ const r = a.column(Report);
474
+ for (let i = 0; i < a.count; i++) {
475
+ r.room[i] = g.room;
476
+ r.hp[i] = g.hp;
477
+ r.coins[i] = g.coins;
478
+ r.hasKey[i] = g.hasKey ? 1 : 0;
479
+ r.doorOpen[i] = g.doorOpen ? 1 : 0;
480
+ r.enemyHp[i] = g.enemyHp;
481
+ r.hits[i] = g.enemyHits;
482
+ r.swings[i] = g.swings;
483
+ r.steps[i] = g.steps;
484
+ r.hurts[i] = g.hurts;
485
+ r.facing[i] = g.facing;
486
+ r.doorWall[i] = map === null ? -1 : og.tilemap.getTile(world, map, "walls", g.doorCellX, g.doorCellY);
487
+ }
488
+ }
489
+ const font = fontOf(g);
490
+ const caps = og.render.capabilities(world);
491
+ const w = caps.width > 0 ? caps.width : 1280;
492
+ const h = caps.height > 0 ? caps.height : 720;
493
+ const size = Math.max(16, Math.round(h * 0.034));
494
+ const pad = Math.round(size * 0.9);
495
+ const line = (text: string, x: number, y: number, color: { r: number; g: number; b: number }, align: "start" | "center" | "end" = "start", s = size) =>
496
+ must(og.render.text(world, { font, text, position: { x, y, z: 0 }, size: s, color, align, screen: true, outline: 0.5, outlineColor: { r: 0.008, g: 0.008, b: 0.011 } }));
497
+ line(`HP ${g.hp}/${g.hpMax}`, pad, pad + size, g.hp > 1 ? WHITE : RED);
498
+ line(`COINS ${g.coins}/${g.coinsTotal}`, pad, pad + size * 2.3, GOLD);
499
+ if (g.hasKey) line("KEY", pad, pad + size * 3.6, GOLD);
500
+ line(`ROOM ${g.room}`, w - pad, pad + size, WHITE, "end");
501
+ if (world.tick < 300 && g.room === 1) line("WASD / arrows move · Space attack · Shift dash", w / 2, h - pad, MINT, "center", Math.round(size * 0.75));
502
+ if (g.hp === 0) line("You fell. Back to the start...", w / 2, h / 2, RED, "center", Math.round(size * 1.2));
503
+ });
@@ -0,0 +1,64 @@
1
+ // The slime's behaviour tree (assets/ai/slime.ogt): with the hero out of
2
+ // sight it patrols its beat on the map's grid; the hero stepping within
3
+ // Tuning.slimeSight halts the patrol at once (the selector is re-checked
4
+ // every tick), the slime chases at Tuning.slimeSpeed and bites in reach
5
+ // through the game's `bite` command, which costs a heart and bounces it back.
6
+ import { playtest } from "@origingame/engine/playtest";
7
+ import { component, position, report } from "./lib.ts";
8
+
9
+ interface AiDump {
10
+ behaviour: string;
11
+ node: string;
12
+ path: string;
13
+ sees: boolean;
14
+ status: string;
15
+ perception: { sees: boolean; sinceSeen: number };
16
+ }
17
+
18
+ export default playtest({
19
+ name: "the slime patrols its beat, chases the hero on sight and bites in reach",
20
+ commands: [
21
+ // The hero waits at the spawn, 9 m away: out of sight.
22
+ "0.5:dump_ai:slime:patrol-a.json",
23
+ "0.5:dump_entity:slime:patrol-a-at.json",
24
+ "2.5:dump_ai:slime:patrol-b.json",
25
+ "2.5:dump_entity:slime:patrol-b-at.json",
26
+ "2.5:dump_nav:slime:patrol-nav.json",
27
+ // The hero steps 3 m from it, in the open.
28
+ "3.0:slime:13.5:6.5",
29
+ "3.0:place:10.5:6.5",
30
+ "3.05:dump_ai:slime:seen.json",
31
+ "3.6:dump_entity:slime:chase-at.json",
32
+ "3.6:dump_ai:slime:chase.json",
33
+ "5.5:dump_entity:hero:bitten.json",
34
+ "5.5:dump_entity:slime:after.json",
35
+ "5.6:quit",
36
+ ].join(";"),
37
+ check(run, t) {
38
+ t.equal(run.warnings.join("; "), "", "the script ran without warnings");
39
+ const a = run.json<AiDump>("patrol-a.json");
40
+ t.equal(a.behaviour, "slime", "the slime runs assets/ai/slime.ogt");
41
+ t.ok(a.path.includes("patrol beat"), `out of sight it patrols (${a.path})`);
42
+ t.equal(a.sees, false, "it does not see the hero at the spawn");
43
+ const pa = position(run, "patrol-a-at.json");
44
+ const pb = position(run, "patrol-b-at.json");
45
+ t.ok(Math.hypot(pb.x - pa.x, pb.y - pa.y) > 0.5, `the patrol moves it (${pa.x.toFixed(2)},${pa.y.toFixed(2)} to ${pb.x.toFixed(2)},${pb.y.toFixed(2)})`);
46
+ t.ok(pb.x >= 13.3 && pb.x <= 17.7 && pb.y >= 3.3 && pb.y <= 6.7, "and keeps it on its beat");
47
+ const nav = run.json<{ status: string; corners: unknown[] }>("patrol-nav.json");
48
+ t.ok(nav.status === "moving" && nav.corners.length >= 2, `og.nav walks it along a path to the next beat point (${nav.status})`);
49
+
50
+ const seen = run.json<AiDump>("seen.json");
51
+ t.equal(seen.sees, true, "three ticks after the hero steps into sight, the slime sees it");
52
+ t.ok(seen.path.includes("chase"), `and the patrol gave way to the chase (${seen.path})`);
53
+ const c = position(run, "chase-at.json");
54
+ t.ok(c.x < 13.0, `it closes in on the hero (x ${c.x.toFixed(2)})`);
55
+ const speed = (13.5 - c.x) / 0.55;
56
+ t.ok(speed > 1.0 && speed < 1.75, `at about Tuning.slimeSpeed, 1.6 m/s (${speed.toFixed(2)})`);
57
+ t.ok(run.json<AiDump>("chase.json").path.includes("chase"), "still chasing");
58
+
59
+ const bitten = report(run, "bitten.json");
60
+ t.ok(bitten.hurts >= 1, `the bite landed (${bitten.hurts} hurt)`);
61
+ t.ok(bitten.hp < 3, "and cost a heart");
62
+ t.ok(component<{ bites: number }>(run, "after.json", "Slime").bites >= 1, "through the slime's bite command");
63
+ },
64
+ });
@@ -0,0 +1,72 @@
1
+ // Combat: the slime's bite costs a heart and knocks the hero back; the
2
+ // sword's strike frame (the attack clip's "hit" event) lands 12, 12, then a
3
+ // 26 crit, and each hit holds the world for Tuning.hitStop ticks with the
4
+ // slime flashed white and squashed; the third kills it, and it fades and
5
+ // hides.
6
+ //
7
+ // A press at t = 0.65 s (tick 39) reaches the strike frame six ticks later
8
+ // (tick 45): frame 0 of the attack lasts 0.07 s, then Control reads the
9
+ // event on the next tick.
10
+ import { playtest } from "@origingame/engine/playtest";
11
+ import { component, position, press, report, scale } from "./lib.ts";
12
+
13
+ /** Hero at x, slime 1.1 m to its right, facing it. */
14
+ const setup = (t: number) => `${t}:place:12.3:6.5;${t}:slime:13.4:6.5;${t}:face:right`;
15
+
16
+ export default playtest({
17
+ name: "a bite hurts the hero; three sword hits kill the slime with hit stop, flash and a crit",
18
+ commands: [
19
+ // The bite: the slime starts on the hero.
20
+ "0.1:place:12.3:6.5",
21
+ "0.1:slime:12.8:6.5",
22
+ // Read after the hit stop: the bite lands about tick 11, the stop holds the
23
+ // hero four ticks, and the knock-back (9 m/s, decaying) has moved it past
24
+ // 0.3 m by 0.3 s (12.3 to 11.80; 11.70 by 0.6 s).
25
+ "0.3:dump_entity:hero:bitten.json",
26
+ // First hit, then two ticks into the stop and the tick after it.
27
+ setup(0.6),
28
+ press("Space", 0.65),
29
+ "0.768:dump_entity:slime:stop-a.json",
30
+ "0.801:dump_entity:slime:stop-b.json",
31
+ "0.851:dump_entity:slime:after.json",
32
+ "0.9:dump_entity:hero:hit1.json",
33
+ setup(1.3),
34
+ press("Space", 1.35),
35
+ "1.6:dump_entity:hero:hit2.json",
36
+ setup(2.0),
37
+ press("Space", 2.05),
38
+ "2.3:dump_entity:hero:hit3.json",
39
+ "3.0:dump_entity:slime:dead.json",
40
+ "3.0:dump_entity:hero:end.json",
41
+ "3.1:quit",
42
+ ].join(";"),
43
+ check(run, t) {
44
+ t.equal(run.warnings.join("; "), "", "the script ran without warnings");
45
+ const bitten = report(run, "bitten.json");
46
+ t.equal(bitten.hp, 2, "the bite cost one heart");
47
+ t.equal(bitten.hurts, 1, "one hurt");
48
+ t.ok(position(run, "bitten.json").x < 12.0, `the bite knocked the hero back (x ${position(run, "bitten.json").x.toFixed(3)})`);
49
+
50
+ // The hit stop: the slime holds its place, white and squashed, then moves off.
51
+ const a = position(run, "stop-a.json");
52
+ const b = position(run, "stop-b.json");
53
+ t.equal(b.x, a.x, "the slime holds still during the hit stop");
54
+ t.equal(component<{ flash: number }>(run, "stop-a.json", "og.Sprite").flash, 1, "the struck slime is white during the stop");
55
+ t.near(scale(run, "stop-a.json").x, 1.45, 0.001, "the struck slime is squashed during the stop");
56
+ t.equal(component<{ hp: number }>(run, "stop-a.json", "Slime").hp, 38, "the first hit dealt 12");
57
+ t.ok(position(run, "after.json").x > b.x + 0.1, "after the stop the knockback carries the slime away");
58
+ t.ok(scale(run, "after.json").x < 1.45, "after the stop the squash recovers");
59
+
60
+ t.equal(report(run, "hit1.json").enemyHp, 38, "Game.enemyHp after one hit");
61
+ t.equal(report(run, "hit2.json").enemyHp, 26, "a second 12");
62
+ const end = report(run, "end.json");
63
+ t.equal(end.enemyHp, 0, "the crit (26) finished it");
64
+ t.equal(end.hits, 3, "three hits");
65
+ t.equal(end.swings, 3, "three strike frames: one per press");
66
+ t.equal(end.hp, 2, "the hero still has two hearts");
67
+ const slime = component<{ dead: boolean }>(run, "dead.json", "Slime");
68
+ t.equal(slime.dead, true, "the slime is dead");
69
+ t.equal(component<{ visible: boolean }>(run, "dead.json", "og.Sprite").visible, false, "the dead slime is hidden");
70
+ t.near(scale(run, "dead.json").y, 0, 0.001, "the death tween squashed it to nothing");
71
+ },
72
+ });