@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.
- package/LICENSE +21 -0
- package/README.md +26 -2
- package/base/_gitignore +10 -0
- package/base/index.html +20 -0
- package/base/package.json +23 -0
- package/base/src/ground.worker.ts +8 -0
- package/base/src/main.ts +61 -0
- package/base/tsconfig.json +14 -0
- package/base/vite.config.ts +25 -0
- package/bin.mjs +10 -0
- package/cli.mjs +381 -0
- package/index.mjs +286 -0
- package/layers/assets/package.json +5 -0
- package/layers/editor/package.json +8 -0
- package/layers/editor/src/editor.ts +13 -0
- package/layers/rooms/server.ts +69 -0
- package/layers/server/package.json +8 -0
- package/layers/server/server.ts +34 -0
- package/layers/source/package.json +12 -0
- package/layers/source/tsconfig.json +5 -0
- package/layers/source/vite.config.ts +45 -0
- package/layers/tests/package.json +8 -0
- package/package.json +46 -5
- package/starters/campaign/README.md +61 -0
- package/starters/campaign/files.txt +42 -0
- package/starters/campaign/layers/editor/src/editor.ts +18 -0
- package/starters/campaign/layers/rooms/server.ts +39 -0
- package/starters/campaign/layers/server/server.ts +33 -0
- package/starters/campaign/layers/tests/tests/rules.test.ts +320 -0
- package/starters/campaign/public/games/campaign/assets.json +1 -0
- package/starters/campaign/src/campaign/campaign.json +52 -0
- package/starters/campaign/src/campaign/carry.ts +47 -0
- package/starters/campaign/src/campaign/cinematics/intro.json +148 -0
- package/starters/campaign/src/campaign/cinematics/outro.json +92 -0
- package/starters/campaign/src/campaign/data.json +1 -0
- package/starters/campaign/src/campaign/fx.json +1 -0
- package/starters/campaign/src/campaign/game.ts +6 -0
- package/starters/campaign/src/campaign/ids.ts +77 -0
- package/starters/campaign/src/campaign/index.ts +121 -0
- package/starters/campaign/src/campaign/kills.ts +16 -0
- package/starters/campaign/src/campaign/looks.json +1 -0
- package/starters/campaign/src/campaign/maps.ts +34 -0
- package/starters/campaign/src/campaign/missions/homecoming.ts +160 -0
- package/starters/campaign/src/campaign/missions/siege.ts +91 -0
- package/starters/campaign/src/campaign/missions/warlord.ts +177 -0
- package/starters/campaign/src/campaign/prefabs/watchpost.prefab.json +18 -0
- package/starters/campaign/src/campaign/scenes.ts +46 -0
- package/starters/campaign/src/campaign/tiles.ts +8 -0
- package/starters/campaign/src/campaign/units.ts +390 -0
- package/starters/campaign/src/campaign/view/cinematics.ts +59 -0
- package/starters/campaign/src/campaign/view/fire.ts +115 -0
- package/starters/campaign/src/campaign/view/index.ts +334 -0
- package/starters/campaign/src/campaign/view/labels.ts +105 -0
- package/starters/campaign/src/campaign/view/rubble.ts +40 -0
- package/starters/campaign/src/campaign/view/waveLane.ts +88 -0
- package/starters/campaign/src/campaign/waves.json +36 -0
- package/starters/campaign/src/campaign/waves.ts +186 -0
- package/starters/campaign/src/campaign-editor.ts +43 -0
- package/starters/campaign/src/game.ts +2 -0
- package/starters/campaign/src/main.ts +70 -0
- package/starters/campaign/src/maps/fort.names.ts +31 -0
- package/starters/campaign/src/maps/fort.vgmap.json +1162 -0
- package/starters/campaign/src/maps/homestead.names.ts +36 -0
- package/starters/campaign/src/maps/homestead.vgmap.json +916 -0
- package/starters/campaign/src/maps/pass.names.ts +40 -0
- package/starters/campaign/src/maps/pass.vgmap.json +522 -0
- package/starters/campaign/src/maps.ts +2 -0
- package/starters/campaign/src/rules.ts +20 -0
- package/starters/campaign/src/waves-panel.ts +355 -0
- package/starters/empty/README.md +71 -0
- package/starters/empty/layers/tests/tests/rules.test.ts +31 -0
- package/starters/empty/src/game.ts +79 -0
- package/starters/empty/src/generate.worker.ts +9 -0
- package/starters/empty/src/maps.ts +44 -0
- package/starters/empty/src/rules.ts +67 -0
- package/starters/empty/src/tiles.ts +12 -0
- package/starters/melee/README.md +61 -0
- package/starters/melee/files.txt +60 -0
- package/starters/melee/layers/editor/src/editor.ts +13 -0
- package/starters/melee/layers/tests/tests/rules.test.ts +58 -0
- package/starters/melee/src/game.ts +2 -0
- package/starters/melee/src/maps/bay.names.ts +28 -0
- package/starters/melee/src/maps/bay.vgmap.json +3123 -0
- package/starters/melee/src/maps.ts +20 -0
- package/starters/melee/src/melee/abilities.ts +376 -0
- package/starters/melee/src/melee/bay.ts +319 -0
- package/starters/melee/src/melee/blight.ts +102 -0
- package/starters/melee/src/melee/cellLayers.json +11 -0
- package/starters/melee/src/melee/data.json +18 -0
- package/starters/melee/src/melee/floats.json +199 -0
- package/starters/melee/src/melee/formations.ts +46 -0
- package/starters/melee/src/melee/fx.json +1 -0
- package/starters/melee/src/melee/game.ts +6 -0
- package/starters/melee/src/melee/generator.ts +116 -0
- package/starters/melee/src/melee/heroSpells.ts +38 -0
- package/starters/melee/src/melee/hud.json +269 -0
- package/starters/melee/src/melee/index.ts +182 -0
- package/starters/melee/src/melee/items.ts +362 -0
- package/starters/melee/src/melee/languages/he.json +96 -0
- package/starters/melee/src/melee/looks.json +1 -0
- package/starters/melee/src/melee/maps.ts +53 -0
- package/starters/melee/src/melee/pings.json +16 -0
- package/starters/melee/src/melee/pings.ts +34 -0
- package/starters/melee/src/melee/quality.json +1 -0
- package/starters/melee/src/melee/saves.ts +12 -0
- package/starters/melee/src/melee/sounds.json +1 -0
- package/starters/melee/src/melee/undead.ts +683 -0
- package/starters/melee/src/melee/units.ts +785 -0
- package/starters/melee/src/melee/view/buildings.ts +33 -0
- package/starters/melee/src/melee/view/cheerLane.ts +42 -0
- package/starters/melee/src/melee/view/heroSpellLooks.ts +21 -0
- package/starters/melee/src/melee/view/index.ts +268 -0
- package/starters/melee/src/melee/view/languages.ts +10 -0
- package/starters/melee/src/melee/view/looks.ts +659 -0
- package/starters/melee/src/melee/view/undeadLooks.ts +220 -0
- package/starters/melee/src/melee/view/walls.ts +59 -0
- package/starters/melee/src/rules.ts +24 -0
- package/starters.mjs +35 -0
- 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
|
+
}
|