@v12-engine/create 0.0.0-stage → 0.1.1

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -2
  3. package/base/_gitignore +10 -0
  4. package/base/index.html +20 -0
  5. package/base/package.json +23 -0
  6. package/base/src/ground.worker.ts +8 -0
  7. package/base/src/main.ts +61 -0
  8. package/base/tsconfig.json +14 -0
  9. package/base/vite.config.ts +25 -0
  10. package/bin.mjs +10 -0
  11. package/cli.mjs +381 -0
  12. package/index.mjs +286 -0
  13. package/layers/assets/package.json +5 -0
  14. package/layers/editor/package.json +8 -0
  15. package/layers/editor/src/editor.ts +13 -0
  16. package/layers/rooms/server.ts +69 -0
  17. package/layers/server/package.json +8 -0
  18. package/layers/server/server.ts +34 -0
  19. package/layers/source/package.json +12 -0
  20. package/layers/source/tsconfig.json +5 -0
  21. package/layers/source/vite.config.ts +45 -0
  22. package/layers/tests/package.json +8 -0
  23. package/package.json +46 -5
  24. package/starters/campaign/README.md +61 -0
  25. package/starters/campaign/files.txt +42 -0
  26. package/starters/campaign/layers/editor/src/editor.ts +18 -0
  27. package/starters/campaign/layers/rooms/server.ts +39 -0
  28. package/starters/campaign/layers/server/server.ts +33 -0
  29. package/starters/campaign/layers/tests/tests/rules.test.ts +320 -0
  30. package/starters/campaign/public/games/campaign/assets.json +1 -0
  31. package/starters/campaign/src/campaign/campaign.json +52 -0
  32. package/starters/campaign/src/campaign/carry.ts +47 -0
  33. package/starters/campaign/src/campaign/cinematics/intro.json +148 -0
  34. package/starters/campaign/src/campaign/cinematics/outro.json +92 -0
  35. package/starters/campaign/src/campaign/data.json +1 -0
  36. package/starters/campaign/src/campaign/fx.json +1 -0
  37. package/starters/campaign/src/campaign/game.ts +6 -0
  38. package/starters/campaign/src/campaign/ids.ts +77 -0
  39. package/starters/campaign/src/campaign/index.ts +121 -0
  40. package/starters/campaign/src/campaign/kills.ts +16 -0
  41. package/starters/campaign/src/campaign/looks.json +1 -0
  42. package/starters/campaign/src/campaign/maps.ts +34 -0
  43. package/starters/campaign/src/campaign/missions/homecoming.ts +160 -0
  44. package/starters/campaign/src/campaign/missions/siege.ts +91 -0
  45. package/starters/campaign/src/campaign/missions/warlord.ts +177 -0
  46. package/starters/campaign/src/campaign/prefabs/watchpost.prefab.json +18 -0
  47. package/starters/campaign/src/campaign/scenes.ts +46 -0
  48. package/starters/campaign/src/campaign/tiles.ts +8 -0
  49. package/starters/campaign/src/campaign/units.ts +390 -0
  50. package/starters/campaign/src/campaign/view/cinematics.ts +59 -0
  51. package/starters/campaign/src/campaign/view/fire.ts +115 -0
  52. package/starters/campaign/src/campaign/view/index.ts +334 -0
  53. package/starters/campaign/src/campaign/view/labels.ts +105 -0
  54. package/starters/campaign/src/campaign/view/rubble.ts +40 -0
  55. package/starters/campaign/src/campaign/view/waveLane.ts +88 -0
  56. package/starters/campaign/src/campaign/waves.json +36 -0
  57. package/starters/campaign/src/campaign/waves.ts +186 -0
  58. package/starters/campaign/src/campaign-editor.ts +43 -0
  59. package/starters/campaign/src/game.ts +2 -0
  60. package/starters/campaign/src/main.ts +70 -0
  61. package/starters/campaign/src/maps/fort.names.ts +31 -0
  62. package/starters/campaign/src/maps/fort.vgmap.json +1162 -0
  63. package/starters/campaign/src/maps/homestead.names.ts +36 -0
  64. package/starters/campaign/src/maps/homestead.vgmap.json +916 -0
  65. package/starters/campaign/src/maps/pass.names.ts +40 -0
  66. package/starters/campaign/src/maps/pass.vgmap.json +522 -0
  67. package/starters/campaign/src/maps.ts +2 -0
  68. package/starters/campaign/src/rules.ts +20 -0
  69. package/starters/campaign/src/waves-panel.ts +355 -0
  70. package/starters/empty/README.md +71 -0
  71. package/starters/empty/layers/tests/tests/rules.test.ts +31 -0
  72. package/starters/empty/src/game.ts +79 -0
  73. package/starters/empty/src/generate.worker.ts +9 -0
  74. package/starters/empty/src/maps.ts +44 -0
  75. package/starters/empty/src/rules.ts +67 -0
  76. package/starters/empty/src/tiles.ts +12 -0
  77. package/starters/melee/README.md +61 -0
  78. package/starters/melee/files.txt +60 -0
  79. package/starters/melee/layers/editor/src/editor.ts +13 -0
  80. package/starters/melee/layers/tests/tests/rules.test.ts +58 -0
  81. package/starters/melee/src/game.ts +2 -0
  82. package/starters/melee/src/maps/bay.names.ts +28 -0
  83. package/starters/melee/src/maps/bay.vgmap.json +3123 -0
  84. package/starters/melee/src/maps.ts +20 -0
  85. package/starters/melee/src/melee/abilities.ts +376 -0
  86. package/starters/melee/src/melee/bay.ts +319 -0
  87. package/starters/melee/src/melee/blight.ts +102 -0
  88. package/starters/melee/src/melee/cellLayers.json +11 -0
  89. package/starters/melee/src/melee/data.json +18 -0
  90. package/starters/melee/src/melee/floats.json +199 -0
  91. package/starters/melee/src/melee/formations.ts +46 -0
  92. package/starters/melee/src/melee/fx.json +1 -0
  93. package/starters/melee/src/melee/game.ts +6 -0
  94. package/starters/melee/src/melee/generator.ts +116 -0
  95. package/starters/melee/src/melee/heroSpells.ts +38 -0
  96. package/starters/melee/src/melee/hud.json +269 -0
  97. package/starters/melee/src/melee/index.ts +182 -0
  98. package/starters/melee/src/melee/items.ts +362 -0
  99. package/starters/melee/src/melee/languages/he.json +96 -0
  100. package/starters/melee/src/melee/looks.json +1 -0
  101. package/starters/melee/src/melee/maps.ts +53 -0
  102. package/starters/melee/src/melee/pings.json +16 -0
  103. package/starters/melee/src/melee/pings.ts +34 -0
  104. package/starters/melee/src/melee/quality.json +1 -0
  105. package/starters/melee/src/melee/saves.ts +12 -0
  106. package/starters/melee/src/melee/sounds.json +1 -0
  107. package/starters/melee/src/melee/undead.ts +683 -0
  108. package/starters/melee/src/melee/units.ts +785 -0
  109. package/starters/melee/src/melee/view/buildings.ts +33 -0
  110. package/starters/melee/src/melee/view/cheerLane.ts +42 -0
  111. package/starters/melee/src/melee/view/heroSpellLooks.ts +21 -0
  112. package/starters/melee/src/melee/view/index.ts +268 -0
  113. package/starters/melee/src/melee/view/languages.ts +10 -0
  114. package/starters/melee/src/melee/view/looks.ts +659 -0
  115. package/starters/melee/src/melee/view/undeadLooks.ts +220 -0
  116. package/starters/melee/src/melee/view/walls.ts +59 -0
  117. package/starters/melee/src/rules.ts +24 -0
  118. package/starters.mjs +35 -0
  119. package/v12.mjs +111 -0
@@ -0,0 +1,220 @@
1
+ import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
2
+ import { Color3 } from '@babylonjs/core/Maths/math.color.js'
3
+ import { CreateCylinder } from '@babylonjs/core/Meshes/Builders/cylinderBuilder.js'
4
+ import type { AbilityLooks, ModelDef, ShotContext, ShotDrawing, UnitLooks } from '@v12-engine/client'
5
+ import { building } from './buildings'
6
+
7
+ /*
8
+ * How the Restless looks (features-plan C4): the shared art's skeletons with their own gear, and its Fantasy RTS
9
+ * buildings, each another than the humans' of its kind, all darkened. Dressed in bones in C6
10
+ */
11
+
12
+ const BONE: [number, number, number] = [0.86, 0.83, 0.74]
13
+ const STONE: [number, number, number] = [0.42, 0.4, 0.44]
14
+ const GOLD: [number, number, number] = [1, 0.8, 0.2]
15
+ const WOOD: [number, number, number] = [0.45, 0.31, 0.18]
16
+
17
+ /**
18
+ * A skeleton of the shared art (`characters/skeleton_*.glb`), `scale` times as big, with what it holds of the skeletons'
19
+ * gear in each hand (`skeleton-gear/*.glb`). Each file by its whole path: a build copies the files of the shared art
20
+ * that the code names, and a path put together from parts names none
21
+ */
22
+ const skeleton = (src: string, scale: number, right?: string, left?: string): ModelDef => ({
23
+ src,
24
+ scale,
25
+ hold: [...(right ? [{ src: right, at: 'handslot.r' }] : []), ...(left ? [{ src: left, at: 'handslot.l' }] : [])]
26
+ })
27
+
28
+ /** Bones, for what is drawn while a model loads */
29
+ const bones = (height: number) => [
30
+ {
31
+ shape: 'cylinder' as const,
32
+ size: [0.5, height * 0.6, 0.5] as [number, number, number],
33
+ offset: [0, height * 0.3, 0] as [number, number, number],
34
+ color: BONE
35
+ },
36
+ {
37
+ shape: 'sphere' as const,
38
+ size: [0.4, 0.4, 0.4] as [number, number, number],
39
+ offset: [0, height * 0.75, 0] as [number, number, number],
40
+ color: BONE
41
+ },
42
+ {
43
+ shape: 'box' as const,
44
+ size: [0.3, 0.12, 0.3] as [number, number, number],
45
+ offset: [0, height * 0.45, 0] as [number, number, number],
46
+ color: 'team' as const
47
+ }
48
+ ]
49
+
50
+ /** A block of dark stone with a roof in the owner's colour, for what is drawn while a building's model loads */
51
+ const crypt = (size: number, height: number) => [
52
+ {
53
+ shape: 'box' as const,
54
+ size: [size, height * 0.6, size] as [number, number, number],
55
+ offset: [0, height * 0.3, 0] as [number, number, number],
56
+ color: STONE
57
+ },
58
+ {
59
+ shape: 'pyramid' as const,
60
+ size: [size * 1.1, height * 0.4, size * 1.1] as [number, number, number],
61
+ offset: [0, height * 0.8, 0] as [number, number, number],
62
+ color: 'team' as const
63
+ }
64
+ ]
65
+
66
+ export const undeadLooks: UnitLooks = {
67
+ acolyte: {
68
+ blood: false,
69
+ height: 1.5,
70
+ model: skeleton('characters/skeleton_minion.glb', 0.6, 'skeleton-gear/staff.glb'),
71
+ parts: [
72
+ ...bones(1.5),
73
+ { shape: 'sphere', size: [0.42, 0.36, 0.42], offset: [0, 0.75, -0.34], color: GOLD, carrying: 'gold' },
74
+ { shape: 'cylinder', size: [0.22, 0.9, 0.22], offset: [0, 0.8, -0.34], color: WOOD, tilt: Math.PI / 2, turn: Math.PI / 2, carrying: 'lumber' }
75
+ ]
76
+ },
77
+ ghoul: {
78
+ blood: false,
79
+ height: 1.6,
80
+ model: skeleton('characters/skeleton_minion.glb', 0.68, 'skeleton-gear/blade.glb', 'skeleton-gear/blade.glb'),
81
+ // packs of them: drawn as copies of one mesh, their animations baked (S41)
82
+ baked: true,
83
+ parts: bones(1.6)
84
+ },
85
+ cryptCrawler: {
86
+ blood: false,
87
+ height: 1.7,
88
+ model: {
89
+ ...skeleton('characters/skeleton_rogue.glb', 0.75, 'skeleton-gear/crossbow.glb'),
90
+ animations: { attack: 'Ranged_2H_Shoot' },
91
+ hitAt: 0.3
92
+ },
93
+ shot: { color: [0.55, 0.85, 0.45], size: 0.25 },
94
+ parts: bones(1.7)
95
+ },
96
+ abomination: {
97
+ blood: false,
98
+ height: 2.6,
99
+ model: skeleton('characters/skeleton_warrior.glb', 1.2, 'skeleton-gear/axe.glb', 'skeleton-gear/shield_large_a.glb'),
100
+ parts: bones(2.6)
101
+ },
102
+ necromancer: {
103
+ blood: false,
104
+ height: 1.8,
105
+ model: { ...skeleton('characters/skeleton_mage.glb', 0.75, 'skeleton-gear/staff.glb'), animations: { attack: 'Ranged_Magic_Shoot' } },
106
+ shot: { color: [0.55, 0.85, 0.45], size: 0.3 },
107
+ parts: bones(1.8)
108
+ },
109
+ gargoyle: {
110
+ blood: false,
111
+ height: 1.6,
112
+ model: skeleton('characters/skeleton_minion.glb', 0.75, 'skeleton-gear/axe.glb'),
113
+ altitude: 3.2,
114
+ shot: { color: [0.6, 0.6, 0.65], size: 0.3 },
115
+ parts: bones(1.6)
116
+ },
117
+ deathKnight: {
118
+ blood: false,
119
+ height: 2.3,
120
+ model: skeleton('characters/skeleton_warrior.glb', 0.95, 'skeleton-gear/blade.glb', 'skeleton-gear/shield_small_a.glb'),
121
+ parts: bones(2.3)
122
+ },
123
+ lich: {
124
+ blood: false,
125
+ height: 2.2,
126
+ model: { ...skeleton('characters/skeleton_mage.glb', 1, 'skeleton-gear/staff.glb'), animations: { attack: 'Ranged_Magic_Shoot' } },
127
+ shot: { color: [0.55, 0.85, 1], size: 0.35 },
128
+ parts: bones(2.2)
129
+ },
130
+ gravehold: { height: 4.6, model: building('rts/wooden_temple.glb', 3.6), parts: crypt(3.6, 4.6) },
131
+ hallOfBones: { height: 5.6, model: building('rts/fortress.glb', 3.7), parts: crypt(3.7, 5.6) },
132
+ duskCitadel: { height: 6.6, model: building('rts/wooden_fortress.glb', 3.9), parts: crypt(3.8, 6.6) },
133
+ // over the gold mine's own model, a little bigger
134
+ cursedMine: { height: 3.4, model: building('rts/mine.glb', 4), parts: crypt(4, 3.4) },
135
+ boneSpire: { height: 2.4, model: building('rts/wooden_monument.glb', 1.9), parts: crypt(1.7, 2.4) },
136
+ crypt: { height: 3.2, model: building('rts/storage_house.glb', 2.7), parts: crypt(2.7, 3.2) },
137
+ graveyard: { height: 3, model: building('rts/houses.glb', 2.7), parts: crypt(2.7, 3) },
138
+ darkAltar: { height: 3.6, model: building('rts/temple.glb', 2.8), parts: crypt(2.8, 3.6) },
139
+ slaughterhouse: { height: 3.4, model: building('rts/storage_shed.glb', 2.7), parts: crypt(2.7, 3.4) },
140
+ temple: { height: 3.6, model: building('rts/temple_2.glb', 2.6), parts: crypt(2.6, 3.6) },
141
+ spiritTower: {
142
+ height: 4.4,
143
+ model: building('rts/stone_tower_2.glb', 1.9, { points: { launch: { offset: [0, 3, 0] } } }),
144
+ // a beam of the dead's green from the tower's top to where the shot is: drawn by melee, not the client
145
+ shot: { draw: beam([0.55, 0.95, 0.45]) },
146
+ parts: crypt(1.5, 4.4)
147
+ },
148
+ tomb: { height: 3.2, model: building('rts/archery_towers.glb', 2.7), parts: crypt(2.7, 3.2) }
149
+ }
150
+
151
+ /** What the Restless' buildings are darkened with */
152
+ const DARKENED: [number, number, number] = [0.38, 0.26, 0.42]
153
+
154
+ // the Restless' buildings are summoned: they rise up out of the ground as they go up, rather than grow
155
+ for (const id of [
156
+ 'gravehold',
157
+ 'hallOfBones',
158
+ 'duskCitadel',
159
+ 'cursedMine',
160
+ 'boneSpire',
161
+ 'crypt',
162
+ 'graveyard',
163
+ 'darkAltar',
164
+ 'slaughterhouse',
165
+ 'temple',
166
+ 'spiritTower',
167
+ 'tomb'
168
+ ]) {
169
+ const look = undeadLooks[id]
170
+ look.motion = { construction: (progress, root) => void (root.position.y -= (1 - progress) * look.height) }
171
+ // the human buildings it borrows, darkened to the Restless' bruised purple: every copy, the model itself
172
+ if (look.model) look.model = { ...look.model, tint: DARKENED }
173
+ }
174
+
175
+ export const undeadEffects: AbilityLooks = {
176
+ deathCoil: { color: [0.55, 0.85, 0.45] },
177
+ animateDead: { color: [0.55, 0.85, 0.45], style: 'burst', options: { count: 80, rise: 2.5, shape: 'rise' } },
178
+ darkCharge: { color: [0.4, 0.3, 0.55] },
179
+ unholy: { color: [0.55, 0.85, 0.45] },
180
+ frostNova: { color: [0.55, 0.85, 1], style: 'burst', options: { count: 60, ring: 2.5, shape: 'about' } },
181
+ frostArmor: { color: [0.62, 0.86, 1] },
182
+ chilled: { color: [0.62, 0.86, 1] },
183
+ deathAndDecay: { color: [0.45, 0.6, 0.3] },
184
+ unholyFrenzy: { color: [0.9, 0.3, 0.25] },
185
+ crippled: { color: [0.5, 0.35, 0.6] },
186
+ webbed: { color: [0.9, 0.9, 0.85] },
187
+ web: { color: [0.9, 0.9, 0.85] }
188
+ }
189
+
190
+ /**
191
+ * A shot drawn as a beam from where it left to where it is, glowing, in a colour: a shot of the game's own drawing
192
+ * (`ShotDef.draw`), which the client flies and tells where it is
193
+ */
194
+ function beam(color: [number, number, number]) {
195
+ return ({ kit, from, projectile }: ShotContext): ShotDrawing => {
196
+ const mesh = CreateCylinder(`beam-${projectile.id}`, { height: 1, diameter: 0.18, tessellation: 6 }, kit.scene)
197
+ const material = new StandardMaterial(`beam-${projectile.id}`, kit.scene)
198
+ material.disableLighting = true
199
+ material.emissiveColor = new Color3(...color)
200
+ material.alpha = 0.75
201
+ mesh.material = material
202
+ mesh.isPickable = false
203
+ return {
204
+ move(at) {
205
+ const dx = at.x - from.x
206
+ const dy = at.y - from.y
207
+ const dz = at.z - from.z
208
+ const length = Math.hypot(dx, dy, dz)
209
+ // from where it left to where it is: a cylinder stood on its side, between the two
210
+ mesh.position.set((from.x + at.x) / 2, (from.y + at.y) / 2, (from.z + at.z) / 2)
211
+ mesh.scaling.set(1, Math.max(length, 0.01), 1)
212
+ mesh.rotation.set(Math.acos(Math.max(-1, Math.min(1, dy / Math.max(length, 1e-6)))), Math.atan2(dx, dz), 0)
213
+ },
214
+ dispose() {
215
+ mesh.dispose()
216
+ material.dispose()
217
+ }
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,59 @@
1
+ import type { GameView, MinimapLayer } from '@v12-engine/client'
2
+ import { wallGate, wallGateCommand } from '@v12-engine/engine'
3
+ import { blightLayer } from '../blight'
4
+
5
+ /** The game's buttons of the command card, as the view takes them */
6
+ type CardButtons = NonNullable<NonNullable<GameView['hud']>['cardButtons']>
7
+
8
+ /**
9
+ * The button of a gate of the player's (`withWalls`, `wall.gate`): Open while it is shut, Shut while it is open, for
10
+ * everyone (`wallGateCommand`). On the card of the gate picked first, once it stands
11
+ */
12
+ export const gateButtons: CardButtons = ({ primary }) => {
13
+ const gate = primary?.own ? (primary.state.components?.[wallGate.id] as { open: boolean } | undefined) : undefined
14
+ if (!primary || !gate) return []
15
+ return [
16
+ {
17
+ id: 'gate',
18
+ label: gate.open ? 'Shut' : 'Open',
19
+ description: gate.open ? 'Shut the gate: nobody walks through' : 'Open the gate: anyone walks through, friend or foe',
20
+ hotkey: gate.open ? 'S' : 'O',
21
+ command: wallGateCommand.make({ entity: primary.id, open: !gate.open })
22
+ }
23
+ ]
24
+ }
25
+
26
+ /** The game's card buttons together, in order */
27
+ export const cardButtons =
28
+ (...parts: readonly CardButtons[]): CardButtons =>
29
+ (context) =>
30
+ parts.flatMap((part) => part(context))
31
+
32
+ /**
33
+ * The blight on the minimap (`blightLayer`): its cells tinted in its colour on the ground, under the dots and the fog,
34
+ * as it spreads. A run of cells a row is one rectangle
35
+ */
36
+ export function blightMinimapLayer(): MinimapLayer {
37
+ const [r, g, b] = blightLayer.color ?? [0.45, 0.3, 0.55]
38
+ const fill = `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, 0.6)`
39
+ return {
40
+ id: 'blight',
41
+ at: 'ground',
42
+ draw(context, { toMap, scale, client }) {
43
+ const { terrain } = client.cellLayers
44
+ const cells = terrain.cells[blightLayer.id]
45
+ if (!cells) return
46
+ context.fillStyle = fill
47
+ for (let z = 0; z < terrain.depth; z++) {
48
+ const row = z * terrain.width
49
+ for (let x = 0; x < terrain.width; x++) {
50
+ if (!cells[row + x]) continue
51
+ const from = x
52
+ while (x + 1 < terrain.width && cells[row + x + 1]) x++
53
+ const at = toMap(from, z)
54
+ context.fillRect(at.x, at.y, (x + 1 - from) * scale.x, scale.y)
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The rules: melee's, in `src/melee`. Its races, heroes, units, buildings and the generator of its random maps are
3
+ * in that folder; this is the name every part of the project knows them by
4
+ */
5
+ import type { GameMap, PlayerId, World } from '@v12-engine/engine'
6
+ import { melee } from './melee'
7
+
8
+ export const rules = melee
9
+
10
+ /**
11
+ * What the game does once a world is made from a map, before anyone plays. Every player with a start is given a hall
12
+ * and five workers by the rules themselves; in the page, the computer plays everyone but you, each against the other
13
+ * teams' starts. A server seats its computers itself (`server.ts`), so there is no one to seat here
14
+ */
15
+ export function start(world: World, { map, player }: { map?: GameMap; player?: PlayerId }): void {
16
+ if (!map || player === undefined) return
17
+ const players = map.players.filter((other) => other.start)
18
+ for (const other of players) {
19
+ if (other.id === player) continue
20
+ const enemyBases = players.filter((rival) => rival.team !== other.team).map((rival) => rival.start!)
21
+ // remembered, so that a game loaded from a save of this one plays on against them
22
+ world.computers.seat(other.id, { enemyBases, settings: { army: { firstAttack: 360, waveSize: 6 } } })
23
+ }
24
+ }
package/starters.mjs ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The games a project starts from (`--starter`), in the order the command line offers them. `needs`: the options it
3
+ * cannot be made without (melee and the campaign play with the shared art). `generator`: its random map's preview is
4
+ * made in a worker of the page's (`src/generate.worker.ts`, the `@if generator` lines of the shared `main.ts`)
5
+ */
6
+ export const starters = [
7
+ {
8
+ name: 'empty',
9
+ title: 'Hello world',
10
+ summary: 'a flat field and a scout to walk it, nothing more: the smallest game to grow your own from',
11
+ generator: true
12
+ },
13
+ {
14
+ name: 'melee',
15
+ title: 'Melee',
16
+ summary: 'a whole RTS: two races and their heroes, bases and armies, against the computer',
17
+ needs: ['assets']
18
+ },
19
+ {
20
+ name: 'campaign',
21
+ title: 'A campaign',
22
+ summary: 'three missions with a hero carried between them: a base to defend, a siege of waves at night, a warlord to face',
23
+ needs: ['assets']
24
+ }
25
+ ]
26
+
27
+ /** The starter the command line makes a project from when nothing says which */
28
+ export const DEFAULT_STARTER = 'empty'
29
+
30
+ /** A starter by name, or an error that lists them */
31
+ export function starterNamed(name) {
32
+ const found = starters.find((starter) => starter.name === name)
33
+ if (!found) throw new Error(`There is no starter "${name}": the starters are ${starters.map((starter) => starter.name).join(', ')}`)
34
+ return found
35
+ }
package/v12.mjs ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `v12`: a project's own commands, from its folder (`npm run map …` in a project made with `npm create @v12-engine`).
4
+ *
5
+ * v12 map check [map …] | picture <map> <out.png> | picture [folder] --all | diff <before> <after> <out.png> | diff <before> <after> --text | run <script.ts> <map>
6
+ * | merge <base> <ours> <theirs> | generate <game> <name> [--generator id] [--seed N] …
7
+ * v12 save diff <a> <b>
8
+ *
9
+ * The project is read as its page reads it, through its own Vite, so its game (`src/game.ts`: every game defined there,
10
+ * `defineGame`) and a script for `map run` are TypeScript with nothing built first. Its maps are in `src/maps`
11
+ */
12
+ import { readFileSync } from 'node:fs'
13
+ import { createRequire } from 'node:module'
14
+ import { join, resolve } from 'node:path'
15
+ import { pathToFileURL } from 'node:url'
16
+
17
+ const [command, ...args] = process.argv.slice(2)
18
+
19
+ /** The project's games: every game defined in its `src/game.ts` (what `defineGame` gives: an id, rules and a view) */
20
+ function gamesOf(module) {
21
+ const games = {}
22
+ for (const value of Object.values(module)) {
23
+ if (value && typeof value === 'object' && typeof value.id === 'string' && value.rules && value.view) {
24
+ games[value.id] = { module: value.rules, view: value.view }
25
+ }
26
+ }
27
+ return games
28
+ }
29
+
30
+ async function map(project) {
31
+ // the project's own Vite, as its page builds with
32
+ const require = createRequire(join(project, 'package.json'))
33
+ const { createServer } = await import(pathToFileURL(require.resolve('vite')).href)
34
+ const server = await createServer({
35
+ root: project,
36
+ server: { middlewareMode: true, hmr: false, watch: null },
37
+ appType: 'custom',
38
+ logLevel: 'error'
39
+ })
40
+ try {
41
+ const games = gamesOf(await server.ssrLoadModule('/src/game.ts'))
42
+ if (!Object.keys(games).length) throw new Error('src/game.ts defines no game (`export const game = defineGame({ rules, view })`)')
43
+ // the map editor's: a project made without the editor has no map command
44
+ try {
45
+ require.resolve('@v12-engine/editor/map-command')
46
+ } catch {
47
+ throw new Error("v12 map is the map editor's: this project has none. npm install @v12-engine/editor")
48
+ }
49
+ const { mapCommand } = await server.ssrLoadModule('@v12-engine/editor/map-command')
50
+ return await mapCommand(args, {
51
+ games,
52
+ mapsOf: () => join(project, 'src/maps'),
53
+ load: (path) => server.ssrLoadModule(path),
54
+ name: 'npm run map',
55
+ root: project
56
+ })
57
+ } finally {
58
+ await server.close()
59
+ }
60
+ }
61
+
62
+ /** A save as a file keeps it: what `encodeSave` writes, or something that holds that text as its `save` (a saved game, a scenario) */
63
+ function saveIn(engine, path) {
64
+ const text = readFileSync(path, 'utf8')
65
+ const read = JSON.parse(text)
66
+ return typeof read?.save === 'string' ? engine.decodeSave(read.save) : engine.decodeSave(text)
67
+ }
68
+
69
+ /** A value of a save, short enough for a line */
70
+ const shown = (value) => {
71
+ const text = value === undefined ? 'nothing' : JSON.stringify(value)
72
+ return text.length > 200 ? `${text.slice(0, 200)}…` : text
73
+ }
74
+
75
+ /**
76
+ * `v12 save diff a b`: where two saves of a game first differ, as a replay that came apart says it (the engine's
77
+ * `saveDifference`): the path (`entities[id 11].pos.x`) and what each holds there. Exits 1 when they differ, 0 when not
78
+ */
79
+ async function save(project) {
80
+ const [what, ...files] = args
81
+ if (what !== 'diff' || files.length !== 2) {
82
+ console.log('v12 save diff <a> <b>: where two saves of a game first differ (an entity and its field), exit 1 if they do')
83
+ return what === 'help' || what === undefined ? 0 : 1
84
+ }
85
+ const require = createRequire(join(project, 'package.json'))
86
+ const engine = await import(pathToFileURL(require.resolve('@v12-engine/engine')).href)
87
+ const [a, b] = files.map((file) => saveIn(engine, file))
88
+ const found = engine.saveDifference(a, b)
89
+ if (!found) {
90
+ console.log('The two saves are the same')
91
+ return 0
92
+ }
93
+ console.log(`They differ first at ${found.path || 'the top'}: ${shown(found.a)} in ${files[0]}, ${shown(found.b)} in ${files[1]}`)
94
+ return 1
95
+ }
96
+
97
+ try {
98
+ // npm runs a script from the project's folder; the paths it was given are where it was asked from
99
+ const project = resolve(process.env.npm_config_local_prefix ?? process.cwd())
100
+ if (process.env.INIT_CWD) process.chdir(process.env.INIT_CWD)
101
+ if (command === 'map') process.exitCode = await map(project)
102
+ else if (command === 'save') process.exitCode = await save(project)
103
+ else {
104
+ console.log('v12 map …: the maps of this project from the command line (v12 map help)')
105
+ console.log('v12 save diff <a> <b>: where two saves of a game first differ')
106
+ process.exitCode = command ? 1 : 0
107
+ }
108
+ } catch (error) {
109
+ console.error(error instanceof Error ? error.message : error)
110
+ process.exitCode = 1
111
+ }