@v12-engine/create 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.
- 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 +45 -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,91 @@
|
|
|
1
|
+
import { entityDied } from '@v12-engine/engine'
|
|
2
|
+
import { defineListener, defineObjective, defineSequence, setGate, talk, wait, type World } from '@v12-engine/engine'
|
|
3
|
+
import { ALDRIC, BRANN, ENEMY, MSG, PLAYER, UNIT } from '../ids'
|
|
4
|
+
import { siegeWaves, startWaves, waveBeaten, waveComing } from '../waves'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Mission two, The Siege: the Red Hand comes for the fort at night, in waves out of the war camp (`waves.ts`, the
|
|
8
|
+
* editor's Waves panel says who). Hold for every wave, keep the hall; a tower lost costs the optional objective. The
|
|
9
|
+
* gate shuts as a wave sets out and opens between waves; the rules fire effects where things happen, and the map has
|
|
10
|
+
* its torches and its bonfire
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const holdOut = defineObjective({ id: 'campaign.hold', text: MSG.objectiveHold })
|
|
14
|
+
export const keepTheHall = defineObjective({ id: 'campaign.siegeHall', text: MSG.objectiveHall })
|
|
15
|
+
export const keepTowers = defineObjective({ id: 'campaign.towers', text: MSG.objectiveTowers, required: false })
|
|
16
|
+
|
|
17
|
+
/** The gate of the fort, a doodad of the map by that name (`gate: true` in the editor) */
|
|
18
|
+
export const GATE = 'gate'
|
|
19
|
+
|
|
20
|
+
/** The briefing: what is coming, and what the gate does */
|
|
21
|
+
export const siegeBriefing = defineSequence('campaign.siegeBriefing', [
|
|
22
|
+
wait(0.5),
|
|
23
|
+
...talk(PLAYER, [
|
|
24
|
+
{ speaker: BRANN, text: MSG.siegeStart, seconds: 5 },
|
|
25
|
+
{ speaker: ALDRIC, text: MSG.siegeGate, seconds: 5 }
|
|
26
|
+
])
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/** A wave sets out: the gate shuts, the camp flares (an effect fired by the rules, seen by every screen) */
|
|
30
|
+
export const waveSetsOut = defineListener({
|
|
31
|
+
event: waveComing,
|
|
32
|
+
id: 'campaign.waveSetsOut',
|
|
33
|
+
run(world, { x, z }) {
|
|
34
|
+
if (!world.objectives.has(PLAYER, holdOut.id)) return
|
|
35
|
+
world.effect('spark-burst', { at: { x, z }, params: { size: 2 } })
|
|
36
|
+
if (setGate(world, GATE, false)) world.message(PLAYER, MSG.gateShut, 'info')
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const halfway = defineSequence('campaign.siegeHalfway', talk(PLAYER, [{ speaker: BRANN, text: MSG.siegeHalfway, seconds: 4 }]))
|
|
41
|
+
const beforeTheLast = defineSequence('campaign.siegeLast', talk(PLAYER, [{ speaker: ALDRIC, text: MSG.siegeLast, seconds: 5 }]))
|
|
42
|
+
|
|
43
|
+
/** A wave beaten: the gate opens, the objective moves along; after the last, the mission is won */
|
|
44
|
+
export const waveHeld = defineListener({
|
|
45
|
+
event: waveBeaten,
|
|
46
|
+
id: 'campaign.waveHeld',
|
|
47
|
+
run(world, { wave }) {
|
|
48
|
+
const { objectives } = world
|
|
49
|
+
if (!objectives.has(PLAYER, holdOut.id)) return
|
|
50
|
+
objectives.progress(PLAYER, holdOut.id, wave, { params: { wave } })
|
|
51
|
+
if (wave >= siegeWaves(world).length) {
|
|
52
|
+
objectives.complete(PLAYER, holdOut.id)
|
|
53
|
+
objectives.complete(PLAYER, keepTheHall.id)
|
|
54
|
+
if (objectives.get(PLAYER, keepTowers.id).status === 'open') objectives.complete(PLAYER, keepTowers.id)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
if (setGate(world, GATE, true)) world.message(PLAYER, MSG.gateOpen, 'info')
|
|
58
|
+
if (wave === Math.floor(siegeWaves(world).length / 2)) world.sequences.start(halfway)
|
|
59
|
+
else if (wave === siegeWaves(world).length - 1) world.sequences.start(beforeTheLast)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/** Something of the fort's fell: a burst where it stood; a tower, the optional objective; the hall, the mission */
|
|
64
|
+
export const fortBuildingFell = defineListener({
|
|
65
|
+
event: entityDied,
|
|
66
|
+
id: 'campaign.fortBuildingFell',
|
|
67
|
+
run(world, { entity }) {
|
|
68
|
+
if (entity.owner !== PLAYER || entity.def.kind !== 'building' || !world.objectives.has(PLAYER, holdOut.id)) return
|
|
69
|
+
world.effect('explosion', { at: { x: entity.pos.x, z: entity.pos.z }, params: { size: entity.def.id === UNIT.wall ? 0.8 : 1.5 } })
|
|
70
|
+
if (entity.def.id === UNIT.tower) world.objectives.fail(PLAYER, keepTowers.id)
|
|
71
|
+
if (entity.def.id === UNIT.hall) world.objectives.fail(PLAYER, keepTheHall.id, MSG.hallFell)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const siegeSequences = [siegeBriefing, halfway, beforeTheLast]
|
|
76
|
+
export const siegeListeners = [waveSetsOut, waveHeld, fortBuildingFell]
|
|
77
|
+
|
|
78
|
+
/** Gives the mission: once, as the world is made. The waves, their timers and the gate's state are kept in a save */
|
|
79
|
+
export function siege(world: World, fresh: boolean): void {
|
|
80
|
+
if (!fresh) return
|
|
81
|
+
const { objectives } = world
|
|
82
|
+
objectives.add(PLAYER, holdOut, { params: { wave: 0, waves: siegeWaves(world).length }, progress: { value: 0, max: siegeWaves(world).length } })
|
|
83
|
+
objectives.add(PLAYER, keepTheHall)
|
|
84
|
+
objectives.add(PLAYER, keepTowers)
|
|
85
|
+
// the Red Hand of the war camp has nothing to pay with, and sees nothing it has not come to
|
|
86
|
+
const purse = world.player(ENEMY).resources
|
|
87
|
+
for (const key of Object.keys(purse)) purse[key] = 0
|
|
88
|
+
setGate(world, GATE, true)
|
|
89
|
+
startWaves(world)
|
|
90
|
+
world.sequences.start(siegeBriefing)
|
|
91
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { cinematicChoice as cinematicChoiceEvent, enteredArea, entityDied } from '@v12-engine/engine'
|
|
2
|
+
import {
|
|
3
|
+
applyBuff,
|
|
4
|
+
areaShapeOf,
|
|
5
|
+
cinematic,
|
|
6
|
+
defineComponent,
|
|
7
|
+
defineListener,
|
|
8
|
+
defineObjective,
|
|
9
|
+
defineSequence,
|
|
10
|
+
MAP_NEUTRAL,
|
|
11
|
+
playCinematic,
|
|
12
|
+
talk,
|
|
13
|
+
wait,
|
|
14
|
+
waitFor,
|
|
15
|
+
type World
|
|
16
|
+
} from '@v12-engine/engine'
|
|
17
|
+
import { guard, holdPosition, missionBrain } from '@v12-engine/engine/ai'
|
|
18
|
+
import { ALDRIC, BRANN, MSG, PLAYER, UNIT, ENEMY } from '../ids'
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Mission three, The Warlord: Aldric, as he left the siege (the campaign carries him: his level, his abilities, what
|
|
22
|
+
* he bought), up the pass to Varga's hold. A trader on the way sells what he can carry. At the hold's gate the two
|
|
23
|
+
* talk, and the player chooses: face Varga alone, his men standing down, or charge with everyone. Won when Varga
|
|
24
|
+
* falls; lost when Aldric does; the prisoners in the cells are the optional one
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const slayWarlord = defineObjective({ id: 'campaign.warlord', text: MSG.objectiveWarlord })
|
|
28
|
+
export const keepHero = defineObjective({ id: 'campaign.hero', text: MSG.objectiveHero })
|
|
29
|
+
export const freePrisoners = defineObjective({ id: 'campaign.prisoners', text: MSG.objectivePrisoners, required: false })
|
|
30
|
+
|
|
31
|
+
/** The areas of the map the rules watch: the hold's gate, where the talk is, and the cells */
|
|
32
|
+
const GATE = 'campaign.holdGate'
|
|
33
|
+
const CELLS = 'campaign.cells'
|
|
34
|
+
const REGIONS: [string, string][] = [
|
|
35
|
+
[GATE, 'gate'],
|
|
36
|
+
[CELLS, 'cells']
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/** The cinematic of the talk, by its id in the view, and what the choice in it is called */
|
|
40
|
+
export const PARLEY = 'campaign.parley'
|
|
41
|
+
export const PARLEY_CHOICE = 'parley'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What the player answered at the gate, kept in the world (so a save keeps it): what the campaign carries on as the
|
|
45
|
+
* mission is won (`carry.ts`). Nothing for a talk skipped, or not had yet
|
|
46
|
+
*/
|
|
47
|
+
export const parley = defineComponent<{ choice?: 'duel' | 'charge' }>('campaign.parley', { default: () => ({}) })
|
|
48
|
+
|
|
49
|
+
/** Varga's men: they hold the hold, and his guard stays by him */
|
|
50
|
+
export const vargasMen = missionBrain({
|
|
51
|
+
id: 'campaign.vargasMen',
|
|
52
|
+
modules: [guard('warlord', { units: { named: ['guard1', 'guard2', 'guard3'] } }), holdPosition('hold')]
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/** The briefing, and the trader pointed out */
|
|
56
|
+
export const passBriefing = defineSequence('campaign.passBriefing', [
|
|
57
|
+
wait(0.5),
|
|
58
|
+
...talk(PLAYER, [
|
|
59
|
+
{ speaker: BRANN, text: MSG.passStart, seconds: 5 },
|
|
60
|
+
{ speaker: BRANN, text: MSG.passShop, seconds: 5 }
|
|
61
|
+
])
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* At the gate: the talk, as a cinematic the rules play for the player (`playCinematic`, handed who talks), the game
|
|
66
|
+
* held while it plays. The choice in it comes back as a command of the player's (`cinematicChoice`), which
|
|
67
|
+
* `theChoice` hears; a cinematic with no data to hand over is the `cinematic` step, waited for or not
|
|
68
|
+
*/
|
|
69
|
+
export const atTheGate = defineSequence('campaign.atTheGate', [
|
|
70
|
+
waitFor(enteredArea, (_world, { area, entity }) => area === GATE && entity.owner === PLAYER),
|
|
71
|
+
(world) => {
|
|
72
|
+
const hero = world.named('hero')
|
|
73
|
+
const warlord = world.named('warlord')
|
|
74
|
+
playCinematic(world, PARLEY, { to: PLAYER, hold: true, params: { hero: hero?.id, warlord: warlord?.id } })
|
|
75
|
+
}
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
/** Varga's guards, by the names the map gives them */
|
|
79
|
+
const guards = (world: World) => ['guard1', 'guard2', 'guard3'].flatMap((name) => world.named(name) ?? [])
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The player's answer: a duel, and the guards stand down (neutral: they watch), Varga the stronger for it; a charge,
|
|
83
|
+
* and the guards come for the hero at once. A choice skipped is no choice: they fight as they would
|
|
84
|
+
*/
|
|
85
|
+
export const theChoice = defineListener({
|
|
86
|
+
event: cinematicChoiceEvent,
|
|
87
|
+
id: 'campaign.theChoice',
|
|
88
|
+
run(world, { cinematic: id, choice, player }) {
|
|
89
|
+
if (id !== PARLEY || player !== PLAYER || !world.objectives.has(PLAYER, slayWarlord.id)) return
|
|
90
|
+
const warlord = world.named('warlord')
|
|
91
|
+
const hero = world.named('hero')
|
|
92
|
+
world.state(parley).choice = choice === 'duel' ? 'duel' : 'charge'
|
|
93
|
+
if (choice === 'duel') {
|
|
94
|
+
for (const man of guards(world)) world.setOwner(man, MAP_NEUTRAL)
|
|
95
|
+
if (warlord?.alive) applyBuff(world, warlord, 'rallied', 60)
|
|
96
|
+
world.sequences.start(duel)
|
|
97
|
+
} else {
|
|
98
|
+
if (hero?.alive)
|
|
99
|
+
for (const man of guards(world)) world.issue({ type: 'order', player: ENEMY, entities: [man.id], order: { type: 'attack', target: hero.id } })
|
|
100
|
+
world.sequences.start(charge)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const duel = defineSequence('campaign.duel', talk(PLAYER, [{ speaker: ALDRIC, text: MSG.passDuel, seconds: 4 }]))
|
|
106
|
+
const charge = defineSequence('campaign.charge', talk(PLAYER, [{ speaker: BRANN, text: MSG.passCharge, seconds: 4 }]))
|
|
107
|
+
|
|
108
|
+
/** The prisoners' cells reached: they are the player's from then on */
|
|
109
|
+
export const cellsOpened = defineListener({
|
|
110
|
+
event: enteredArea,
|
|
111
|
+
id: 'campaign.cellsOpened',
|
|
112
|
+
run(world, { area, entity }) {
|
|
113
|
+
if (area !== CELLS || entity.owner !== PLAYER || !world.objectives.has(PLAYER, freePrisoners.id)) return
|
|
114
|
+
if (world.objectives.get(PLAYER, freePrisoners.id).status !== 'open') return
|
|
115
|
+
for (const prisoner of [...world.index.ownedBy(MAP_NEUTRAL)])
|
|
116
|
+
if (prisoner.alive && prisoner.def.id === UNIT.prisoner) world.setOwner(prisoner, PLAYER)
|
|
117
|
+
world.objectives.complete(PLAYER, freePrisoners.id)
|
|
118
|
+
world.sequences.start(freed)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const freed = defineSequence('campaign.freed', talk(PLAYER, [{ speaker: BRANN, text: MSG.passPrisoners, seconds: 4 }]))
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Varga falls: the outro plays, the game held while it does, and then it is won (the end screen comes once the
|
|
126
|
+
* outro is over, as the game stands still under it). Aldric falls: it is lost
|
|
127
|
+
*/
|
|
128
|
+
export const theEnd = defineSequence('campaign.theEnd', [
|
|
129
|
+
waitFor(entityDied, (_world, { entity }) => entity.def.id === UNIT.warlord),
|
|
130
|
+
wait(1.5),
|
|
131
|
+
cinematic('campaign.outro', { hold: true }),
|
|
132
|
+
wait(0.5),
|
|
133
|
+
(world) => {
|
|
134
|
+
const { objectives } = world
|
|
135
|
+
objectives.complete(PLAYER, slayWarlord.id)
|
|
136
|
+
objectives.complete(PLAYER, keepHero.id)
|
|
137
|
+
if (objectives.get(PLAYER, freePrisoners.id).status === 'open') objectives.fail(PLAYER, freePrisoners.id)
|
|
138
|
+
}
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
export const heroFell = defineListener({
|
|
142
|
+
event: entityDied,
|
|
143
|
+
id: 'campaign.heroFell',
|
|
144
|
+
run(world, { entity }) {
|
|
145
|
+
if (entity.def.id !== UNIT.knight || entity.owner !== PLAYER || !world.objectives.has(PLAYER, keepHero.id)) return
|
|
146
|
+
if (world.objectives.get(PLAYER, slayWarlord.id).status === 'open') world.objectives.fail(PLAYER, keepHero.id, MSG.heroFell)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
export const warlordSequences = [passBriefing, atTheGate, duel, charge, freed, theEnd]
|
|
151
|
+
export const warlordListeners = [theChoice, cellsOpened, heroFell]
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Gives the mission: once, as the world is made. On the campaign's difficulty (`MissionContext.difficulty`; normal from
|
|
155
|
+
* the editor's Play here): on easy Varga starts worn, three quarters of his health; on hard a brute stands by him
|
|
156
|
+
* besides his guard. The Red Hand's computer plays on it as well, which the campaign's scene sees to
|
|
157
|
+
*/
|
|
158
|
+
export function warlord(world: World, fresh: boolean, difficulty = 'normal'): void {
|
|
159
|
+
if (!fresh) return
|
|
160
|
+
const { objectives } = world
|
|
161
|
+
objectives.add(PLAYER, slayWarlord)
|
|
162
|
+
objectives.add(PLAYER, keepHero)
|
|
163
|
+
objectives.add(PLAYER, freePrisoners)
|
|
164
|
+
for (const [id, region] of REGIONS) {
|
|
165
|
+
const shape = areaShapeOf({ region }, world.map.regions)
|
|
166
|
+
if (shape) world.areas.add({ id, shape })
|
|
167
|
+
}
|
|
168
|
+
const purse = world.player(ENEMY).resources
|
|
169
|
+
for (const key of Object.keys(purse)) purse[key] = 0
|
|
170
|
+
const varga = world.named('warlord')
|
|
171
|
+
if (varga && difficulty === 'easy') varga.hp = Math.round(varga.maxHp * 0.75)
|
|
172
|
+
if (varga && difficulty === 'hard') world.spawn(UNIT.brute, ENEMY, { x: varga.pos.x + 2, z: varga.pos.z + 1 }, { name: 'guard4' })
|
|
173
|
+
world.computers.seat(ENEMY, { brain: vargasMen })
|
|
174
|
+
world.sequences.start(passBriefing)
|
|
175
|
+
world.sequences.start(atTheGate)
|
|
176
|
+
world.sequences.start(theEnd)
|
|
177
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "watchpost",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"tags": ["defence"],
|
|
5
|
+
"contents": {
|
|
6
|
+
"kind": "v12-clip",
|
|
7
|
+
"objects": [
|
|
8
|
+
{ "id": 1, "type": "tower", "owner": 1, "x": 0, "z": 0, "facing": 0, "scale": 1 },
|
|
9
|
+
{ "id": 2, "type": "archer", "owner": 1, "x": -1.5, "z": 1.5, "facing": 3.14, "scale": 1 },
|
|
10
|
+
{ "id": 3, "type": "archer", "owner": 1, "x": 1.5, "z": 1.5, "facing": 3.14, "scale": 1 }
|
|
11
|
+
],
|
|
12
|
+
"camps": [],
|
|
13
|
+
"regions": [],
|
|
14
|
+
"points": [],
|
|
15
|
+
"paths": [],
|
|
16
|
+
"doodads": []
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineCampaign } from '@v12-engine/client/app'
|
|
2
|
+
import campaignFile from './campaign.json'
|
|
3
|
+
import { campaign, campaignMap, homecoming, homecomingCarry, type RoadData, siege, siegeCarriedIn, warlord, warlordCarry } from './index'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The campaign of three missions, Aldric carried from one to the next: `defineCampaign` gives the menu (where the
|
|
7
|
+
* player is, the missions won with their times, the saves), the briefing before a mission and the debrief after one
|
|
8
|
+
* (where it says them) and the scene that plays any mission; a mission won is kept at once, its end offers the next
|
|
9
|
+
* and a retry. What is data is its file, `campaign.json` (the editor's Campaign panel): its chapters, and each mission's
|
|
10
|
+
* name, map, words, briefing and debrief. What is code is here, by mission id: each mission's `start` gives its rules
|
|
11
|
+
* once (`fresh`); a game loaded from a save has them already. What else goes on is the campaign's own data
|
|
12
|
+
* (`RoadData`): each mission's `carry` gives its part as it is won, the next reads it as it starts (`progress.data`)
|
|
13
|
+
*/
|
|
14
|
+
export const theLongRoad = defineCampaign<RoadData>({
|
|
15
|
+
file: campaignFile,
|
|
16
|
+
game: campaign,
|
|
17
|
+
// its look (and the client that draws it) fetched as its menu opens: not part of the start screen's code
|
|
18
|
+
view: () => import('./view').then((module) => module.campaignView),
|
|
19
|
+
// a mission's map by the name the file gives it: `homestead` is `src/maps/homestead.vgmap.json`
|
|
20
|
+
maps: campaignMap,
|
|
21
|
+
missions: [
|
|
22
|
+
{
|
|
23
|
+
id: 'homecoming',
|
|
24
|
+
// its debrief's words are the file's; its rating, by how quick it was, is code
|
|
25
|
+
debrief: { rating: ({ seconds }) => (seconds < 600 ? 3 : seconds < 1200 ? 2 : 1) },
|
|
26
|
+
start: (world, { fresh }) => homecoming(world, fresh),
|
|
27
|
+
// the footmen who live through it, and whether the caravan was found
|
|
28
|
+
carry: (world) => homecomingCarry(world)
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'siege',
|
|
32
|
+
start: (world, { fresh, map, progress }) => {
|
|
33
|
+
siege(world, fresh)
|
|
34
|
+
// the footmen of Homecoming man the fort; a game loaded from a save has them already
|
|
35
|
+
if (fresh) siegeCarriedIn(world, map, progress.data)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'warlord',
|
|
40
|
+
// harder or easier with the campaign's difficulty, chosen in its menu
|
|
41
|
+
start: (world, { fresh, difficulty }) => warlord(world, fresh, difficulty),
|
|
42
|
+
// the answer at the gate: the last mission, so the campaign keeps it for whatever comes after
|
|
43
|
+
carry: (world) => warlordCarry(world)
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { presetTiles } from '@v12-engine/engine'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What the ground is painted with: the engine's tiles of an RTS (grass, dark grass, dirt, road, sand, rock, snow),
|
|
5
|
+
* drawn with the client's `TILE_LOOKS`. A tile of the game's own goes beside them: `defineTile({ id, name })` here,
|
|
6
|
+
* its look in the view
|
|
7
|
+
*/
|
|
8
|
+
export const tiles = [...presetTiles]
|