@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,390 @@
|
|
|
1
|
+
import type { EffectFields } from '@v12-engine/engine'
|
|
2
|
+
import { type AbilityDef, type BuffDef, defineEffect, type EffectKind, type ItemDef, type Scaled, scaled, type UnitTypeDef } from '@v12-engine/engine'
|
|
3
|
+
import { UNIT } from './ids'
|
|
4
|
+
|
|
5
|
+
// the game's own effects, each with the fields it reads (`EffectTypes`)
|
|
6
|
+
declare module '@v12-engine/engine' {
|
|
7
|
+
interface EffectTypes {
|
|
8
|
+
'campaign.judgement': Pick<EffectFields, 'amount' | 'scaling'>
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* What there is: the player's workers, soldiers and buildings, the hero, the Red Hand, the creeps of the woods, and
|
|
14
|
+
* what the maps are dressed with. Each is data (`UnitTypeDef`): what the editor's Data panel changes of it is kept in
|
|
15
|
+
* `data.json` (`editable`, in index.ts), and the look of each is the view's (`view/index.ts`)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ---- the player's
|
|
19
|
+
|
|
20
|
+
/** Digs gold at a mine and carries it home, and puts up the buildings */
|
|
21
|
+
export const peasant: UnitTypeDef = {
|
|
22
|
+
id: UNIT.peasant,
|
|
23
|
+
name: 'Peasant',
|
|
24
|
+
kind: 'unit',
|
|
25
|
+
radius: 0.4,
|
|
26
|
+
stats: { health: 220, attackDamage: 6, attackSpeed: 0.6, attackRange: 0.4, moveSpeed: 4 },
|
|
27
|
+
sight: 8,
|
|
28
|
+
attacks: [{ damagePoint: 0.4, acquisitionRange: 0 }],
|
|
29
|
+
cost: { gold: 60 },
|
|
30
|
+
buildTime: 8,
|
|
31
|
+
harvester: { capacity: 10, gatherTime: 1.5 },
|
|
32
|
+
builds: [UNIT.farm, UNIT.barracks, UNIT.tower, UNIT.wall],
|
|
33
|
+
hotkey: 'P'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Sword and shield: the line that holds */
|
|
37
|
+
export const footman: UnitTypeDef = {
|
|
38
|
+
id: UNIT.footman,
|
|
39
|
+
name: 'Footman',
|
|
40
|
+
kind: 'unit',
|
|
41
|
+
radius: 0.45,
|
|
42
|
+
stats: { health: 420, attackDamage: 14, attackSpeed: 1.1, attackRange: 0.5, armor: 12, moveSpeed: 4 },
|
|
43
|
+
sight: 10,
|
|
44
|
+
attacks: [{ damagePoint: 0.45, acquisitionRange: 8 }],
|
|
45
|
+
cost: { gold: 90 },
|
|
46
|
+
buildTime: 12,
|
|
47
|
+
hotkey: 'F'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Shoots over the line, and falls fast when reached */
|
|
51
|
+
export const archer: UnitTypeDef = {
|
|
52
|
+
id: UNIT.archer,
|
|
53
|
+
name: 'Archer',
|
|
54
|
+
kind: 'unit',
|
|
55
|
+
radius: 0.42,
|
|
56
|
+
stats: { health: 260, attackDamage: 18, attackSpeed: 1, attackRange: 7, moveSpeed: 4.2 },
|
|
57
|
+
sight: 11,
|
|
58
|
+
attacks: [{ damagePoint: 0.5, acquisitionRange: 8, projectileSpeed: 26 }],
|
|
59
|
+
cost: { gold: 110 },
|
|
60
|
+
buildTime: 14,
|
|
61
|
+
hotkey: 'A'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The hero: Aldric, the map names him `hero`. He grows with every level (`hero.growth`), learns three abilities with
|
|
66
|
+
* his skill points, carries six items, and goes from one mission to the next with all of it (the campaign carries
|
|
67
|
+
* him: `carryOut`, `carryIn`)
|
|
68
|
+
*/
|
|
69
|
+
export const knight: UnitTypeDef = {
|
|
70
|
+
id: UNIT.knight,
|
|
71
|
+
name: 'Knight',
|
|
72
|
+
kind: 'unit',
|
|
73
|
+
radius: 0.5,
|
|
74
|
+
stats: {
|
|
75
|
+
health: 650,
|
|
76
|
+
healthRegen: 1,
|
|
77
|
+
mana: 200,
|
|
78
|
+
manaRegen: 1.5,
|
|
79
|
+
attackDamage: 42,
|
|
80
|
+
attackSpeed: 0.8,
|
|
81
|
+
attackRange: 0.6,
|
|
82
|
+
armor: 30,
|
|
83
|
+
moveSpeed: 4.4
|
|
84
|
+
},
|
|
85
|
+
sight: 12,
|
|
86
|
+
attacks: [{ damagePoint: 0.4, acquisitionRange: 8 }],
|
|
87
|
+
hero: {
|
|
88
|
+
growth: { stats: { health: 80, mana: 30, attackDamage: 3, armor: 3 } },
|
|
89
|
+
learns: ['holyLight', 'warCry', 'judgement'],
|
|
90
|
+
maxLevel: 10
|
|
91
|
+
},
|
|
92
|
+
inventory: 6,
|
|
93
|
+
level: 1,
|
|
94
|
+
hotkey: 'K'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const building = (def: Omit<UnitTypeDef, 'kind'>): UnitTypeDef => ({ kind: 'building', ...def })
|
|
98
|
+
|
|
99
|
+
/** The hall: it trains peasants, and takes the gold they bring. The mission is lost when it falls */
|
|
100
|
+
export const hall = building({
|
|
101
|
+
id: UNIT.hall,
|
|
102
|
+
name: 'Town Hall',
|
|
103
|
+
radius: 2,
|
|
104
|
+
footprint: { w: 4, d: 4 },
|
|
105
|
+
stats: { health: 2200, armor: 40 },
|
|
106
|
+
sight: 12,
|
|
107
|
+
trains: [UNIT.peasant],
|
|
108
|
+
dropOff: true,
|
|
109
|
+
hotkey: 'H'
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
/** A farm: it feeds the men, and is the first thing a raid burns */
|
|
113
|
+
export const farm = building({
|
|
114
|
+
id: UNIT.farm,
|
|
115
|
+
name: 'Farm',
|
|
116
|
+
radius: 1,
|
|
117
|
+
footprint: { w: 2, d: 2 },
|
|
118
|
+
stats: { health: 600, armor: 20 },
|
|
119
|
+
sight: 6,
|
|
120
|
+
cost: { gold: 80 },
|
|
121
|
+
buildTime: 12,
|
|
122
|
+
hotkey: 'F'
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/** The barracks: footmen and archers */
|
|
126
|
+
export const barracks = building({
|
|
127
|
+
id: UNIT.barracks,
|
|
128
|
+
name: 'Barracks',
|
|
129
|
+
radius: 1.5,
|
|
130
|
+
footprint: { w: 3, d: 3 },
|
|
131
|
+
stats: { health: 1600, armor: 30 },
|
|
132
|
+
sight: 9,
|
|
133
|
+
cost: { gold: 180 },
|
|
134
|
+
buildTime: 25,
|
|
135
|
+
trains: [UNIT.footman, UNIT.archer],
|
|
136
|
+
hotkey: 'B'
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
/** A guard tower: it shoots what comes near */
|
|
140
|
+
export const tower = building({
|
|
141
|
+
id: UNIT.tower,
|
|
142
|
+
name: 'Guard Tower',
|
|
143
|
+
radius: 1,
|
|
144
|
+
footprint: { w: 2, d: 2 },
|
|
145
|
+
stats: { health: 1100, attackDamage: 40, attackSpeed: 0.9, attackRange: 9, armor: 40 },
|
|
146
|
+
sight: 12,
|
|
147
|
+
attacks: [{ damagePoint: 0.3, acquisitionRange: 9, projectileSpeed: 24 }],
|
|
148
|
+
cost: { gold: 140 },
|
|
149
|
+
buildTime: 20,
|
|
150
|
+
hotkey: 'T'
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
/** A stretch of wall: nothing walks through it until it is down */
|
|
154
|
+
export const wall = building({
|
|
155
|
+
id: UNIT.wall,
|
|
156
|
+
name: 'Wall',
|
|
157
|
+
radius: 1,
|
|
158
|
+
footprint: { w: 2, d: 1 },
|
|
159
|
+
stats: { health: 700, armor: 60 },
|
|
160
|
+
sight: 2,
|
|
161
|
+
cost: { gold: 25 },
|
|
162
|
+
buildTime: 5,
|
|
163
|
+
hotkey: 'W'
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
/** A trader on the road: sells to whoever comes by, and cannot be hurt */
|
|
167
|
+
export const trader = building({
|
|
168
|
+
id: UNIT.trader,
|
|
169
|
+
name: 'Trader',
|
|
170
|
+
radius: 1.2,
|
|
171
|
+
footprint: { w: 2, d: 2 },
|
|
172
|
+
stats: { health: 1000 },
|
|
173
|
+
invulnerable: true,
|
|
174
|
+
sight: 8,
|
|
175
|
+
sells: ['healingPotion', 'steelSword', 'swiftBoots']
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// ---- the Red Hand's
|
|
179
|
+
|
|
180
|
+
const enemy = (def: Omit<UnitTypeDef, 'kind' | 'sight'> & Partial<Pick<UnitTypeDef, 'sight'>>): UnitTypeDef => ({ kind: 'unit', sight: 8, ...def })
|
|
181
|
+
|
|
182
|
+
/** The common raider: an axe and a shield */
|
|
183
|
+
export const raider = enemy({
|
|
184
|
+
id: UNIT.raider,
|
|
185
|
+
name: 'Raider',
|
|
186
|
+
radius: 0.45,
|
|
187
|
+
stats: { health: 380, attackDamage: 15, attackSpeed: 1, attackRange: 0.5, armor: 8, moveSpeed: 4 },
|
|
188
|
+
attacks: [{ damagePoint: 0.45, acquisitionRange: 8 }],
|
|
189
|
+
level: 1,
|
|
190
|
+
bounty: [6, 10],
|
|
191
|
+
xp: 24
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
/** Throws fire from behind the raiders */
|
|
195
|
+
export const warlock = enemy({
|
|
196
|
+
id: UNIT.warlock,
|
|
197
|
+
name: 'Warlock',
|
|
198
|
+
radius: 0.42,
|
|
199
|
+
stats: { health: 240, attackDamage: 24, attackSpeed: 0.8, attackRange: 7, moveSpeed: 3.8 },
|
|
200
|
+
attacks: [{ damagePoint: 0.5, acquisitionRange: 8, projectileSpeed: 18 }],
|
|
201
|
+
level: 2,
|
|
202
|
+
bounty: [10, 14],
|
|
203
|
+
xp: 36
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
/** Twice a raider, slow, with a two-handed axe that hits all round where it comes down (`splash`: an `impact` event) */
|
|
207
|
+
export const brute = enemy({
|
|
208
|
+
id: UNIT.brute,
|
|
209
|
+
name: 'Brute',
|
|
210
|
+
radius: 0.6,
|
|
211
|
+
stats: { health: 900, attackDamage: 34, attackSpeed: 0.7, attackRange: 0.7, armor: 20, moveSpeed: 3.4 },
|
|
212
|
+
attacks: [{ damagePoint: 0.5, acquisitionRange: 8, splash: 1.2 }],
|
|
213
|
+
level: 4,
|
|
214
|
+
bounty: [24, 30],
|
|
215
|
+
xp: 90
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
/** Varga, who leads them: the last mission is won when he falls */
|
|
219
|
+
export const warlord = enemy({
|
|
220
|
+
id: UNIT.warlord,
|
|
221
|
+
name: 'Warlord',
|
|
222
|
+
radius: 0.7,
|
|
223
|
+
stats: { health: 2400, healthRegen: 2, attackDamage: 60, attackSpeed: 0.8, attackRange: 0.8, armor: 35, moveSpeed: 4 },
|
|
224
|
+
sight: 12,
|
|
225
|
+
attacks: [{ damagePoint: 0.4, acquisitionRange: 9 }],
|
|
226
|
+
level: 8,
|
|
227
|
+
bounty: [200, 250],
|
|
228
|
+
xp: 600
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// ---- the woods'
|
|
232
|
+
|
|
233
|
+
/** The creeps of the forest: in camps by the road, with a bounty for clearing them */
|
|
234
|
+
export const gnoll = enemy({
|
|
235
|
+
id: UNIT.gnoll,
|
|
236
|
+
name: 'Gnoll',
|
|
237
|
+
radius: 0.42,
|
|
238
|
+
stats: { health: 300, attackDamage: 12, attackSpeed: 0.8, attackRange: 0.5, moveSpeed: 4 },
|
|
239
|
+
attacks: [{ damagePoint: 0.4, acquisitionRange: 5 }],
|
|
240
|
+
level: 1,
|
|
241
|
+
bounty: [5, 9],
|
|
242
|
+
xp: 20
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
/** Villagers the Red Hand took: they fight for nobody until they are freed, and then they are the player's */
|
|
246
|
+
export const prisoner: UnitTypeDef = {
|
|
247
|
+
id: UNIT.prisoner,
|
|
248
|
+
name: 'Prisoner',
|
|
249
|
+
kind: 'unit',
|
|
250
|
+
radius: 0.4,
|
|
251
|
+
stats: { health: 160, moveSpeed: 3.8 },
|
|
252
|
+
sight: 6
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** For the look of the maps */
|
|
256
|
+
export const tree: UnitTypeDef = {
|
|
257
|
+
id: UNIT.tree,
|
|
258
|
+
name: 'Tree',
|
|
259
|
+
kind: 'resource',
|
|
260
|
+
radius: 0.5,
|
|
261
|
+
footprint: { w: 1, d: 1 },
|
|
262
|
+
stats: { health: 1 },
|
|
263
|
+
sight: 0,
|
|
264
|
+
invulnerable: true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Where the gold comes from: a peasant goes in with nothing and out with ten */
|
|
268
|
+
export const goldmine: UnitTypeDef = {
|
|
269
|
+
id: UNIT.goldmine,
|
|
270
|
+
name: 'Gold Mine',
|
|
271
|
+
kind: 'resource',
|
|
272
|
+
radius: 2,
|
|
273
|
+
footprint: { w: 4, d: 4 },
|
|
274
|
+
stats: { health: 1 },
|
|
275
|
+
sight: 0,
|
|
276
|
+
invulnerable: true,
|
|
277
|
+
resource: { kind: 'gold', amount: 8000, slots: 1, entered: true }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const units = [
|
|
281
|
+
peasant,
|
|
282
|
+
footman,
|
|
283
|
+
archer,
|
|
284
|
+
knight,
|
|
285
|
+
hall,
|
|
286
|
+
farm,
|
|
287
|
+
barracks,
|
|
288
|
+
tower,
|
|
289
|
+
wall,
|
|
290
|
+
trader,
|
|
291
|
+
raider,
|
|
292
|
+
warlock,
|
|
293
|
+
brute,
|
|
294
|
+
warlord,
|
|
295
|
+
gnoll,
|
|
296
|
+
prisoner,
|
|
297
|
+
tree,
|
|
298
|
+
goldmine
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
// ---- what the hero can do, and what he can carry
|
|
302
|
+
|
|
303
|
+
export const buffs: BuffDef[] = [
|
|
304
|
+
{ id: 'rallied', name: 'Rallied', description: 'Hits 20% harder and 15% faster', percent: { attackDamage: 0.2, attackSpeed: 0.15 } }
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
/** A number a level (`Scaled`): the level's, or the last for a level past the list */
|
|
308
|
+
const atLevel = (amount: Scaled | undefined, level: number): number =>
|
|
309
|
+
Array.isArray(amount) ? amount[Math.min(level, amount.length) - 1] : (amount ?? 0)
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Effects the engine does not have, by the type an ability's effect names (`effects` of the module): a function given
|
|
313
|
+
* the caster, the target and the level, with the world's own damage and healing, so that armour, death, bounty and
|
|
314
|
+
* experience happen as for any hit
|
|
315
|
+
*/
|
|
316
|
+
export const effects: EffectKind[] = [
|
|
317
|
+
/** Judgement: the enemy struck, and the caster healed for what it took */
|
|
318
|
+
defineEffect('campaign.judgement', ({ world, caster, target, level }, effect) => {
|
|
319
|
+
if (!target?.alive) return
|
|
320
|
+
const amount = scaled(world, caster, atLevel(effect.amount, level), effect.scaling, level)
|
|
321
|
+
const before = target.hp
|
|
322
|
+
world.damage(target, amount, caster, { damageType: 'magic' })
|
|
323
|
+
world.heal(caster, Math.max(0, before - Math.max(target.hp, 0)), caster)
|
|
324
|
+
})
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The hero's three abilities, learned a level at a time (`hero.learns`), and the one a potion gives. Two are made of
|
|
329
|
+
* the engine's effects (`heal`, `buff`); Judgement is an effect of the game's own (`effects` above)
|
|
330
|
+
*/
|
|
331
|
+
export const abilities: AbilityDef[] = [
|
|
332
|
+
{
|
|
333
|
+
id: 'holyLight',
|
|
334
|
+
name: 'Holy Light',
|
|
335
|
+
description: 'Heals a friendly unit for 150, 250 or 350',
|
|
336
|
+
hotkey: 'Q',
|
|
337
|
+
target: 'unit',
|
|
338
|
+
targets: { allies: true, self: true },
|
|
339
|
+
manaCost: 50,
|
|
340
|
+
cooldown: 6,
|
|
341
|
+
range: 8,
|
|
342
|
+
castTime: 0.3,
|
|
343
|
+
effects: [{ type: 'heal', amount: [150, 250, 350] }]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: 'warCry',
|
|
347
|
+
name: 'War Cry',
|
|
348
|
+
description: 'Friends within 7 hit harder and faster for 10 seconds',
|
|
349
|
+
hotkey: 'W',
|
|
350
|
+
target: 'none',
|
|
351
|
+
manaCost: 60,
|
|
352
|
+
cooldown: 20,
|
|
353
|
+
effects: [{ type: 'buff', buff: 'rallied', radius: 7, affects: 'allies', duration: 10 }]
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: 'judgement',
|
|
357
|
+
name: 'Judgement',
|
|
358
|
+
description: 'Strikes an enemy for 120, 200 or 280 and heals the knight for as much',
|
|
359
|
+
hotkey: 'E',
|
|
360
|
+
target: 'unit',
|
|
361
|
+
targets: { enemies: true },
|
|
362
|
+
manaCost: 70,
|
|
363
|
+
cooldown: 10,
|
|
364
|
+
range: 6,
|
|
365
|
+
castTime: 0.4,
|
|
366
|
+
effects: [{ type: 'campaign.judgement', amount: [120, 200, 280] }]
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
id: 'drinkPotion',
|
|
370
|
+
name: 'Drink',
|
|
371
|
+
description: 'Heals 200 at once',
|
|
372
|
+
target: 'none',
|
|
373
|
+
cooldown: 1,
|
|
374
|
+
effects: [{ type: 'heal', amount: 200, on: 'caster' }]
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
/** What the trader sells (`trader.sells`) */
|
|
379
|
+
export const items: ItemDef[] = [
|
|
380
|
+
{
|
|
381
|
+
id: 'healingPotion',
|
|
382
|
+
name: 'Healing Potion',
|
|
383
|
+
description: 'Drunk, heals 200. Three in the bottle',
|
|
384
|
+
cost: { gold: 60 },
|
|
385
|
+
charges: 3,
|
|
386
|
+
ability: 'drinkPotion'
|
|
387
|
+
},
|
|
388
|
+
{ id: 'steelSword', name: 'Steel Sword', description: '+12 attack damage', cost: { gold: 180 }, bonus: { stats: { attackDamage: 12 } } },
|
|
389
|
+
{ id: 'swiftBoots', name: 'Swift Boots', description: '+0.8 move speed', cost: { gold: 120 }, bonus: { stats: { moveSpeed: 0.8 } } }
|
|
390
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Cinematic, Cut } from '@v12-engine/client'
|
|
2
|
+
import { ALDRIC, BRANN, VARGA } from '../ids'
|
|
3
|
+
import { PARLEY_CHOICE } from '../missions/warlord'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* The campaign's cinematics in code: a script given a cut (`Cinematic`). The intro and the outro are files made in
|
|
7
|
+
* the editor's Cinematic panel (`../cinematics/*.json`) instead; both kinds are played by id, by the rules
|
|
8
|
+
* (`playCinematic`) or the screen (`client.play`)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The talk at the hold's gate (mission three): the camera on Varga, then on Aldric, a line each, and the player's
|
|
13
|
+
* choice. The answer goes to the rules as a command of the player's (`cinematicChoice`), which branch the fight on
|
|
14
|
+
* it; here it only picks the last line
|
|
15
|
+
*/
|
|
16
|
+
export const parley: Cinematic = async (cut, params) => {
|
|
17
|
+
const { hero, warlord } = (params ?? {}) as { hero?: number; warlord?: number }
|
|
18
|
+
if (hero === undefined || warlord === undefined) return
|
|
19
|
+
await cut.camera({ at: { entity: warlord }, distance: 14, pitch: 0.4, yaw: 0.6 }, 1.2)
|
|
20
|
+
await cut.say(VARGA, 'So the knight walks up to my door, with what is left of his village behind him.', 5)
|
|
21
|
+
await cut.camera({ at: { entity: hero }, distance: 10, pitch: 0.3, yaw: -0.8 }, 1)
|
|
22
|
+
await cut.say(ALDRIC, 'Your camp is ash, Varga. Your siege broke on our walls. This ends here.', 5)
|
|
23
|
+
await cut.camera({ at: { entity: warlord }, distance: 12, pitch: 0.35, yaw: 0.4 }, 1)
|
|
24
|
+
await cut.say(VARGA, 'Then come and end it. Alone, if you have the stomach. Or hide behind your men.', 5)
|
|
25
|
+
const answer = await cut.choose(
|
|
26
|
+
[
|
|
27
|
+
{ id: 'duel', label: 'Face him alone' },
|
|
28
|
+
{ id: 'charge', label: 'Charge, all of us' }
|
|
29
|
+
],
|
|
30
|
+
{ name: PARLEY_CHOICE, question: 'How does this end?', seconds: 15, default: 'charge' }
|
|
31
|
+
)
|
|
32
|
+
if (answer === 'duel') await cut.say(ALDRIC, 'Stand back, all of you. He is mine.', 3)
|
|
33
|
+
else await cut.say(BRANN, 'With me! For the village!', 3)
|
|
34
|
+
await roar(cut, warlord)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The clip Varga roars with, and the moment of it his arms come up */
|
|
38
|
+
const ROAR = 'Ranged_Magic_Spellcasting_Long'
|
|
39
|
+
const ROAR_PEAK = 0.55
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Varga roars as the fight starts: a clip of his model played on the cut (`cut.clip`) with a moment in it (`marks`):
|
|
43
|
+
* at the height of it, sparks burst from him (an effect of the game's, `cut.fx`) and the view shakes
|
|
44
|
+
*/
|
|
45
|
+
async function roar(cut: Cut, warlord: number): Promise<void> {
|
|
46
|
+
await cut.camera({ at: { entity: warlord }, distance: 12, pitch: 0.5 }, 0.6)
|
|
47
|
+
await cut.clip(warlord, ROAR, {
|
|
48
|
+
seconds: 2.2,
|
|
49
|
+
marks: [
|
|
50
|
+
{
|
|
51
|
+
at: ROAR_PEAK,
|
|
52
|
+
run: () => {
|
|
53
|
+
cut.fx('spark-burst', { on: warlord })
|
|
54
|
+
cut.shake(0.8)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
})
|
|
59
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector'
|
|
2
|
+
import { type EffectHandle, type Rgb, Sparks, type SpellStyleFactory } from '@v12-engine/client'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* A spell style of the campaign's own (`styles` in the view, `style: 'campaign.holyFire'` on Judgement's look), as an
|
|
6
|
+
* example of what a game draws with the client's toolkit: particles (`Sparks`, effects of the view's effect system),
|
|
7
|
+
* timed by the style itself
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HALO: Rgb = [1, 0.78, 0.3]
|
|
11
|
+
const LIGHT: Rgb = [1, 0.96, 0.7]
|
|
12
|
+
|
|
13
|
+
/** Seconds the light takes to come down, and how long the ground burns after */
|
|
14
|
+
const FALL = 0.45
|
|
15
|
+
const BURN = 1.5
|
|
16
|
+
/** How wide it comes down on a unit, when the ability says nothing */
|
|
17
|
+
const RADIUS = 1.2
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Holy fire: while the knight winds up, a glow gathers over him and a ring marks the one he strikes; then light rains
|
|
21
|
+
* down there, bursts on the ground, and the ground burns for a moment. All of it particles
|
|
22
|
+
*/
|
|
23
|
+
export const holyFire: SpellStyleFactory = (context) => {
|
|
24
|
+
const sparks = new Sparks(context.fx)
|
|
25
|
+
/** What is still to happen, by the time it happens at (`context.time`) */
|
|
26
|
+
const later: { at: number; run: () => void }[] = []
|
|
27
|
+
const burning: { until: number; stream: EffectHandle }[] = []
|
|
28
|
+
|
|
29
|
+
const groundAt = (x: number, z: number) => context.terrain.heightAt(x, z)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
windup(event) {
|
|
33
|
+
const caster = context.anchors.position(event.caster)
|
|
34
|
+
if (caster)
|
|
35
|
+
sparks.burst(caster.x, caster.y + 2.4, caster.z, {
|
|
36
|
+
color: HALO,
|
|
37
|
+
tint: LIGHT,
|
|
38
|
+
count: 40,
|
|
39
|
+
way: 'about',
|
|
40
|
+
radius: 0.5,
|
|
41
|
+
speed: [0.5, 1.5],
|
|
42
|
+
life: [0.4, event.duration],
|
|
43
|
+
over: event.duration
|
|
44
|
+
})
|
|
45
|
+
context.ring(event.aim.x, event.aim.z, HALO, Math.max(event.size, RADIUS) * 2, 0.5)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
cast(event) {
|
|
49
|
+
const { x, z } = event
|
|
50
|
+
const radius = event.radius ?? RADIUS
|
|
51
|
+
const ground = groundAt(x, z)
|
|
52
|
+
// light coming down on the one struck, long streaks of it
|
|
53
|
+
sparks.burst(x, ground + 9, z, {
|
|
54
|
+
color: HALO,
|
|
55
|
+
tint: LIGHT,
|
|
56
|
+
count: 60,
|
|
57
|
+
way: 'down',
|
|
58
|
+
radius,
|
|
59
|
+
height: 2,
|
|
60
|
+
speed: [14, 20],
|
|
61
|
+
life: [0.45, 0.6],
|
|
62
|
+
size: [0.4, 0.9],
|
|
63
|
+
streaks: true,
|
|
64
|
+
over: 0.3
|
|
65
|
+
})
|
|
66
|
+
later.push({
|
|
67
|
+
at: context.time + FALL,
|
|
68
|
+
run: () => {
|
|
69
|
+
// the burst where it lands, and the ground left burning
|
|
70
|
+
sparks.burst(x, ground + 0.3, z, {
|
|
71
|
+
color: HALO,
|
|
72
|
+
tint: LIGHT,
|
|
73
|
+
count: 50,
|
|
74
|
+
way: 'out',
|
|
75
|
+
radius: radius * 0.5,
|
|
76
|
+
speed: [3, 9],
|
|
77
|
+
life: [0.3, 0.7],
|
|
78
|
+
gravity: 6
|
|
79
|
+
})
|
|
80
|
+
context.ring(x, z, LIGHT, 0.5, radius * 2)
|
|
81
|
+
context.shake?.(x, z, 0.2)
|
|
82
|
+
const stream = sparks.stream(new Vector3(x, ground + 0.15, z), {
|
|
83
|
+
color: HALO,
|
|
84
|
+
tint: LIGHT,
|
|
85
|
+
count: 35,
|
|
86
|
+
way: 'up',
|
|
87
|
+
radius: radius * 0.85,
|
|
88
|
+
speed: [0.6, 2.2],
|
|
89
|
+
life: [0.5, 1.1],
|
|
90
|
+
size: [0.3, 0.8]
|
|
91
|
+
})
|
|
92
|
+
burning.push({ until: context.time + BURN, stream })
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
update() {
|
|
98
|
+
const now = context.time
|
|
99
|
+
for (let i = later.length - 1; i >= 0; i--) {
|
|
100
|
+
if (later[i].at > now) continue
|
|
101
|
+
const [due] = later.splice(i, 1)
|
|
102
|
+
due.run()
|
|
103
|
+
}
|
|
104
|
+
for (let i = burning.length - 1; i >= 0; i--) {
|
|
105
|
+
if (burning[i].until > now) continue
|
|
106
|
+
sparks.release(burning[i].stream)
|
|
107
|
+
burning.splice(i, 1)
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
dispose() {
|
|
112
|
+
sparks.dispose()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|