@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.
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 +45 -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,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]