@v12-engine/create 0.1.0 → 0.4.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/README.md +4 -1
- package/base/_gitignore +5 -0
- package/base/package.json +3 -3
- package/base/src/main.ts +36 -10
- package/base/src/navigation.worker.ts +7 -0
- package/base/vite.config.ts +24 -2
- package/cli.mjs +21 -6
- package/index.mjs +27 -7
- package/layers/desktop/electron/app.mjs +237 -0
- package/layers/desktop/electron/config.mjs +16 -0
- package/layers/desktop/electron/dev.mjs +63 -0
- package/layers/desktop/electron/files.mjs +86 -0
- package/layers/desktop/electron/icons/icon.icns +0 -0
- package/layers/desktop/electron/icons/icon.png +0 -0
- package/layers/desktop/electron/main.mjs +12 -0
- package/layers/desktop/electron/package.mjs +75 -0
- package/layers/desktop/electron/preload.cjs +41 -0
- package/layers/desktop/electron/presentation.m +87 -0
- package/layers/desktop/electron/serve.mjs +76 -0
- package/layers/desktop/electron/tools.mjs +63 -0
- package/layers/desktop/electron/updates.mjs +71 -0
- package/layers/desktop/package.json +13 -0
- package/layers/desktop/src/desktop.ts +175 -0
- package/layers/desktop/src/shell.ts +279 -0
- package/layers/desktop/tests/main.test.ts +189 -0
- package/layers/desktop/tests/shell.test.ts +152 -0
- package/layers/rooms/server.ts +13 -10
- package/layers/server/server.ts +4 -5
- package/layers/source/vite.config.ts +21 -1
- package/package.json +2 -1
- package/starters/campaign/README.md +9 -2
- package/starters/campaign/layers/rooms/server.ts +3 -3
- package/starters/campaign/layers/server/server.ts +3 -5
- package/starters/campaign/layers/tests/tests/rules.test.ts +40 -21
- package/starters/campaign/src/campaign/game.ts +26 -5
- package/starters/campaign/src/campaign/index.ts +104 -68
- package/starters/campaign/src/campaign/kills.ts +1 -1
- package/starters/campaign/src/campaign/missions/homecoming.ts +7 -7
- package/starters/campaign/src/campaign/missions/siege.ts +6 -6
- package/starters/campaign/src/campaign/missions/warlord.ts +13 -14
- package/starters/campaign/src/campaign/scenes.ts +8 -7
- package/starters/campaign/src/campaign/units.ts +59 -20
- package/starters/campaign/src/campaign/view/attacks.ts +19 -0
- package/starters/campaign/src/campaign/view/fire.ts +2 -2
- package/starters/campaign/src/campaign/view/index.ts +54 -43
- package/starters/campaign/src/campaign/waves.ts +18 -17
- package/starters/campaign/src/game.ts +8 -1
- package/starters/campaign/src/main.ts +38 -19
- package/starters/campaign/src/maps/fort.names.ts +2 -2
- package/starters/campaign/src/maps/fort.vgmap.json +102 -2
- package/starters/campaign/src/maps/homestead.names.ts +2 -2
- package/starters/campaign/src/maps/homestead.vgmap.json +85 -1
- package/starters/campaign/src/maps/pass.names.ts +2 -2
- package/starters/campaign/src/maps/pass.vgmap.json +69 -1
- package/starters/campaign/src/waves-panel.ts +2 -1
- package/starters/empty/README.md +18 -10
- package/starters/empty/layers/tests/tests/rules.test.ts +16 -17
- package/starters/empty/src/game.ts +11 -75
- package/starters/empty/src/generate.worker.ts +5 -5
- package/starters/empty/src/rules.ts +83 -25
- package/starters/empty/src/view.ts +82 -0
- package/starters/examples/README.md +83 -0
- package/starters/examples/layers/tests/tests/examples.test.ts +30 -0
- package/starters/examples/package.json +5 -0
- package/starters/examples/src/examples/abilities/areas.ts +165 -0
- package/starters/examples/src/examples/abilities/areasLooks.ts +30 -0
- package/starters/examples/src/examples/abilities/buffs.ts +287 -0
- package/starters/examples/src/examples/abilities/buffsLooks.ts +46 -0
- package/starters/examples/src/examples/abilities/changing.ts +228 -0
- package/starters/examples/src/examples/abilities/changingLooks.ts +61 -0
- package/starters/examples/src/examples/abilities/control.ts +158 -0
- package/starters/examples/src/examples/abilities/controlLooks.ts +78 -0
- package/starters/examples/src/examples/abilities/damage.ts +156 -0
- package/starters/examples/src/examples/abilities/damageLooks.ts +37 -0
- package/starters/examples/src/examples/abilities/family.ts +29 -0
- package/starters/examples/src/examples/abilities/index.ts +32 -0
- package/starters/examples/src/examples/abilities/kit.ts +179 -0
- package/starters/examples/src/examples/abilities/lookKit.ts +71 -0
- package/starters/examples/src/examples/abilities/looks.ts +47 -0
- package/starters/examples/src/examples/abilities/missiles.ts +203 -0
- package/starters/examples/src/examples/abilities/missilesLooks.ts +38 -0
- package/starters/examples/src/examples/abilities/moves.ts +206 -0
- package/starters/examples/src/examples/abilities/movesLooks.ts +52 -0
- package/starters/examples/src/examples/abilities/recasts.ts +181 -0
- package/starters/examples/src/examples/abilities/recastsLooks.ts +41 -0
- package/starters/examples/src/examples/abilities/resources.ts +166 -0
- package/starters/examples/src/examples/abilities/resourcesLooks.ts +42 -0
- package/starters/examples/src/examples/abilities/summons.ts +296 -0
- package/starters/examples/src/examples/abilities/summonsLooks.ts +115 -0
- package/starters/examples/src/examples/abilities/triggers.ts +204 -0
- package/starters/examples/src/examples/abilities/triggersLooks.ts +53 -0
- package/starters/examples/src/examples/abilities/vision.ts +137 -0
- package/starters/examples/src/examples/abilities/visionLooks.ts +50 -0
- package/starters/examples/src/examples/animation/index.ts +235 -0
- package/starters/examples/src/examples/animation/looks.ts +444 -0
- package/starters/examples/src/examples/catalog.ts +52 -0
- package/starters/examples/src/examples/cinematics/index.ts +217 -0
- package/starters/examples/src/examples/cinematics/looks.ts +476 -0
- package/starters/examples/src/examples/cinematics/tour.json +43 -0
- package/starters/examples/src/examples/cinematics/village.ts +88 -0
- package/starters/examples/src/examples/common/looks.ts +65 -0
- package/starters/examples/src/examples/common/units.ts +70 -0
- package/starters/examples/src/examples/construction/index.ts +329 -0
- package/starters/examples/src/examples/construction/looks.ts +275 -0
- package/starters/examples/src/examples/custom-animation/bones.ts +92 -0
- package/starters/examples/src/examples/custom-animation/index.ts +190 -0
- package/starters/examples/src/examples/custom-animation/looks.ts +523 -0
- package/starters/examples/src/examples/custom-animation/runes.ts +82 -0
- package/starters/examples/src/examples/custom-fx/glaive.look.ts +110 -0
- package/starters/examples/src/examples/custom-fx/glaive.ts +40 -0
- package/starters/examples/src/examples/custom-fx/gpu.look.ts +145 -0
- package/starters/examples/src/examples/custom-fx/gpu.ts +22 -0
- package/starters/examples/src/examples/custom-fx/index.ts +25 -0
- package/starters/examples/src/examples/custom-fx/lights.look.ts +144 -0
- package/starters/examples/src/examples/custom-fx/lights.ts +25 -0
- package/starters/examples/src/examples/custom-fx/looks.ts +36 -0
- package/starters/examples/src/examples/custom-fx/meteor.look.ts +133 -0
- package/starters/examples/src/examples/custom-fx/meteor.ts +26 -0
- package/starters/examples/src/examples/custom-fx/parts.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/parts.ts +21 -0
- package/starters/examples/src/examples/custom-fx/portal.look.ts +193 -0
- package/starters/examples/src/examples/custom-fx/portal.ts +48 -0
- package/starters/examples/src/examples/custom-fx/renderer.look.ts +139 -0
- package/starters/examples/src/examples/custom-fx/renderer.ts +18 -0
- package/starters/examples/src/examples/custom-fx/ribbons.look.ts +151 -0
- package/starters/examples/src/examples/custom-fx/ribbons.ts +22 -0
- package/starters/examples/src/examples/custom-fx/shader.ts +42 -0
- package/starters/examples/src/examples/custom-fx/shield.look.ts +226 -0
- package/starters/examples/src/examples/custom-fx/shield.ts +43 -0
- package/starters/examples/src/examples/define.ts +86 -0
- package/starters/examples/src/examples/destruction/index.ts +570 -0
- package/starters/examples/src/examples/destruction/looks.ts +447 -0
- package/starters/examples/src/examples/dialogs/index.ts +383 -0
- package/starters/examples/src/examples/dialogs/looks.ts +546 -0
- package/starters/examples/src/examples/dynamic-map/index.ts +603 -0
- package/starters/examples/src/examples/dynamic-map/looks.ts +301 -0
- package/starters/examples/src/examples/fx/blood.look.ts +62 -0
- package/starters/examples/src/examples/fx/blood.ts +59 -0
- package/starters/examples/src/examples/fx/feel.look.ts +86 -0
- package/starters/examples/src/examples/fx/feel.ts +57 -0
- package/starters/examples/src/examples/fx/fight.ts +28 -0
- package/starters/examples/src/examples/fx/floats.look.ts +108 -0
- package/starters/examples/src/examples/fx/floats.ts +24 -0
- package/starters/examples/src/examples/fx/gallery.look.ts +168 -0
- package/starters/examples/src/examples/fx/gallery.ts +23 -0
- package/starters/examples/src/examples/fx/index.ts +30 -0
- package/starters/examples/src/examples/fx/links.look.ts +83 -0
- package/starters/examples/src/examples/fx/links.ts +49 -0
- package/starters/examples/src/examples/fx/looks.ts +55 -0
- package/starters/examples/src/examples/fx/map.look.ts +93 -0
- package/starters/examples/src/examples/fx/map.ts +33 -0
- package/starters/examples/src/examples/fx/marks.look.ts +100 -0
- package/starters/examples/src/examples/fx/marks.ts +36 -0
- package/starters/examples/src/examples/fx/shots.look.ts +92 -0
- package/starters/examples/src/examples/fx/shots.ts +71 -0
- package/starters/examples/src/examples/fx/spells.look.ts +87 -0
- package/starters/examples/src/examples/fx/spells.ts +122 -0
- package/starters/examples/src/examples/fx/stage.ts +31 -0
- package/starters/examples/src/examples/gui/babylonScreen.ts +167 -0
- package/starters/examples/src/examples/gui/babylonWorld.ts +191 -0
- package/starters/examples/src/examples/gui/htmlPart.ts +95 -0
- package/starters/examples/src/examples/gui/index.ts +143 -0
- package/starters/examples/src/examples/gui/keys.ts +91 -0
- package/starters/examples/src/examples/gui/layout.ts +42 -0
- package/starters/examples/src/examples/gui/looks.ts +270 -0
- package/starters/examples/src/examples/gui/minimap.ts +127 -0
- package/starters/examples/src/examples/gui/ownInterface.ts +129 -0
- package/starters/examples/src/examples/gui/plates.ts +87 -0
- package/starters/examples/src/examples/gui/radial.ts +104 -0
- package/starters/examples/src/examples/gui/solidPart.ts +107 -0
- package/starters/examples/src/examples/looks.ts +45 -0
- package/starters/examples/src/examples/mechanics/battle.ts +51 -0
- package/starters/examples/src/examples/mechanics/damage.ts +80 -0
- package/starters/examples/src/examples/mechanics/flying.ts +93 -0
- package/starters/examples/src/examples/mechanics/formations.ts +58 -0
- package/starters/examples/src/examples/mechanics/highGround.ts +82 -0
- package/starters/examples/src/examples/mechanics/index.ts +44 -0
- package/starters/examples/src/examples/mechanics/looks.ts +289 -0
- package/starters/examples/src/examples/mechanics/maze.ts +119 -0
- package/starters/examples/src/examples/mechanics/orders.ts +68 -0
- package/starters/examples/src/examples/mechanics/selection.ts +30 -0
- package/starters/examples/src/examples/moba/index.ts +377 -0
- package/starters/examples/src/examples/moba/looks.ts +349 -0
- package/starters/examples/src/examples/moba/maps.ts +69 -0
- package/starters/examples/src/examples/moba/spells.ts +34 -0
- package/starters/examples/src/examples/moba/units.ts +196 -0
- package/starters/examples/src/examples/nature/camera.ts +15 -0
- package/starters/examples/src/examples/nature/grass.ts +64 -0
- package/starters/examples/src/examples/nature/index.ts +327 -0
- package/starters/examples/src/examples/nature/land.ts +173 -0
- package/starters/examples/src/examples/nature/looks.ts +577 -0
- package/starters/examples/src/examples/nature/maps.ts +370 -0
- package/starters/examples/src/examples/nature/skies.ts +324 -0
- package/starters/examples/src/examples/nature/weather.ts +80 -0
- package/starters/examples/src/examples/performance/index.ts +263 -0
- package/starters/examples/src/examples/performance/looks.ts +372 -0
- package/starters/examples/src/examples/performance/meter.ts +158 -0
- package/starters/examples/src/examples/post/grades.ts +86 -0
- package/starters/examples/src/examples/post/index.ts +202 -0
- package/starters/examples/src/examples/post/looks.ts +884 -0
- package/starters/examples/src/examples/post/passes.ts +213 -0
- package/starters/examples/src/examples/rts/ai.ts +49 -0
- package/starters/examples/src/examples/rts/airlift.ts +43 -0
- package/starters/examples/src/examples/rts/base.ts +129 -0
- package/starters/examples/src/examples/rts/build.ts +65 -0
- package/starters/examples/src/examples/rts/camps.ts +36 -0
- package/starters/examples/src/examples/rts/carry.ts +103 -0
- package/starters/examples/src/examples/rts/creeps.ts +61 -0
- package/starters/examples/src/examples/rts/data.ts +406 -0
- package/starters/examples/src/examples/rts/ferry.ts +41 -0
- package/starters/examples/src/examples/rts/garrison.ts +50 -0
- package/starters/examples/src/examples/rts/harvest.ts +29 -0
- package/starters/examples/src/examples/rts/hero.ts +67 -0
- package/starters/examples/src/examples/rts/index.ts +77 -0
- package/starters/examples/src/examples/rts/looks.ts +721 -0
- package/starters/examples/src/examples/rts/shop.ts +46 -0
- package/starters/examples/src/examples/rts/tech.ts +39 -0
- package/starters/examples/src/examples/rts/train.ts +48 -0
- package/starters/examples/src/examples/rts/waterside.ts +185 -0
- package/starters/examples/src/examples/showcase/index.ts +714 -0
- package/starters/examples/src/examples/showcase/looks.ts +238 -0
- package/starters/examples/src/game.ts +17 -0
- package/starters/examples/src/main.ts +82 -0
- package/starters/examples/src/rules.ts +31 -0
- package/starters/examples/src/shell/look.ts +79 -0
- package/starters/examples/src/shell/registry.ts +7 -0
- package/starters/examples/src/shell/scenes.ts +178 -0
- package/starters/examples/src/shell/shell.ts +290 -0
- package/starters/examples/src/shell/style.ts +202 -0
- package/starters/examples/src/view.ts +50 -0
- package/starters/melee/README.md +12 -3
- package/starters/melee/layers/tests/tests/heroes.test.ts +402 -0
- package/starters/melee/layers/tests/tests/rules.test.ts +10 -8
- package/starters/melee/src/game.ts +32 -2
- package/starters/melee/src/maps/bay.names.ts +2 -2
- package/starters/melee/src/maps/bay.vgmap.json +198 -1
- package/starters/melee/src/maps.ts +11 -9
- package/starters/melee/src/melee/abilities.ts +1 -194
- package/starters/melee/src/melee/blight.ts +7 -7
- package/starters/melee/src/melee/cellLayers.json +3 -3
- package/starters/melee/src/melee/data.json +1 -18
- package/starters/melee/src/melee/game.ts +6 -4
- package/starters/melee/src/melee/heroes.ts +1025 -0
- package/starters/melee/src/melee/index.ts +64 -61
- package/starters/melee/src/melee/items.ts +15 -362
- package/starters/melee/src/melee/languages/he.json +14 -2
- package/starters/melee/src/melee/pings.ts +5 -6
- package/starters/melee/src/melee/saves.ts +1 -1
- package/starters/melee/src/melee/undead.ts +15 -230
- package/starters/melee/src/melee/units.ts +37 -171
- package/starters/melee/src/melee/view/attacks.ts +19 -0
- package/starters/melee/src/melee/view/heroSpellLooks.ts +39 -8
- package/starters/melee/src/melee/view/index.ts +56 -52
- package/starters/melee/src/melee/view/looks.ts +86 -63
- package/starters/melee/src/melee/view/undeadLooks.ts +70 -31
- package/starters/melee/src/melee/view/walls.ts +1 -1
- package/starters.mjs +6 -0
- package/v12.mjs +14 -15
- package/starters/campaign/src/rules.ts +0 -20
- package/starters/empty/src/maps.ts +0 -44
- package/starters/empty/src/tiles.ts +0 -12
- package/starters/melee/src/melee/heroSpells.ts +0 -38
- package/starters/melee/src/rules.ts +0 -24
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @if tests
|
|
2
|
+
// @vitest-environment happy-dom
|
|
3
|
+
/** The page's side of the shell (`src/shell.ts`), on a pretend bridge in the place of `electron/preload.cjs`'s */
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
autosaveOnClose,
|
|
7
|
+
canQuit,
|
|
8
|
+
checkForUpdate,
|
|
9
|
+
createErrorLog,
|
|
10
|
+
fileStore,
|
|
11
|
+
gameFullscreen,
|
|
12
|
+
inShell,
|
|
13
|
+
quit,
|
|
14
|
+
setWindowed,
|
|
15
|
+
watchWindowed
|
|
16
|
+
} from '../src/shell'
|
|
17
|
+
|
|
18
|
+
function pretendBridge() {
|
|
19
|
+
const files = new Map<string, string>()
|
|
20
|
+
const calls: unknown[] = []
|
|
21
|
+
const listeners = { resized: [] as (() => void)[], close: [] as (() => void)[] }
|
|
22
|
+
let over = false
|
|
23
|
+
let update: { version: string; notes?: string } | undefined
|
|
24
|
+
const bridge = {
|
|
25
|
+
fullscreen: async (on?: boolean) => {
|
|
26
|
+
calls.push(['fullscreen', on])
|
|
27
|
+
if (on !== undefined) over = on
|
|
28
|
+
return over
|
|
29
|
+
},
|
|
30
|
+
close: async () => void calls.push(['close']),
|
|
31
|
+
destroy: async () => void calls.push(['destroy']),
|
|
32
|
+
onResized: (listener: () => void) => (listeners.resized.push(listener), () => (listeners.resized.length = 0)),
|
|
33
|
+
onCloseRequested: (listener: () => void) => (listeners.close.push(listener), () => (listeners.close.length = 0)),
|
|
34
|
+
openFolder: async (folder: string) => void calls.push(['openFolder', folder]),
|
|
35
|
+
records: {
|
|
36
|
+
list: async (folder: string) => [...files].filter(([key]) => key.startsWith(`${folder}/`)).map(([, text]) => text),
|
|
37
|
+
read: async (folder: string, id: string) => files.get(`${folder}/${id}`),
|
|
38
|
+
write: async (folder: string, id: string, text: string) => void files.set(`${folder}/${id}`, text),
|
|
39
|
+
remove: async (folder: string, id: string) => void files.delete(`${folder}/${id}`)
|
|
40
|
+
},
|
|
41
|
+
log: { append: async () => undefined, size: async () => 0, rename: async () => undefined },
|
|
42
|
+
updates: {
|
|
43
|
+
check: async () => (calls.push(['check']), update),
|
|
44
|
+
install: async () => void calls.push(['install'])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
;(window as any).v12Shell = bridge
|
|
48
|
+
return { files, calls, listeners, found: (version: typeof update) => (update = version) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
delete (window as any).v12Shell
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('in a browser', () => {
|
|
56
|
+
it('is nothing of the shell', async () => {
|
|
57
|
+
expect(inShell()).toBe(false)
|
|
58
|
+
expect(canQuit()).toBe(false)
|
|
59
|
+
expect(await gameFullscreen(true)).toBe(false)
|
|
60
|
+
await quit()
|
|
61
|
+
expect(await checkForUpdate()).toBeUndefined()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('in the shell', () => {
|
|
66
|
+
it('asks it for the screen, and is told which it is', async () => {
|
|
67
|
+
const { calls } = pretendBridge()
|
|
68
|
+
expect(inShell()).toBe(true)
|
|
69
|
+
expect(await gameFullscreen(true)).toBe(true)
|
|
70
|
+
expect(await gameFullscreen()).toBe(true)
|
|
71
|
+
await setWindowed(true)
|
|
72
|
+
expect(calls).toEqual([
|
|
73
|
+
['fullscreen', true],
|
|
74
|
+
['fullscreen', undefined],
|
|
75
|
+
['fullscreen', false]
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('keeps records as JSON, and leaves out one that does not read', async () => {
|
|
80
|
+
const { files } = pretendBridge()
|
|
81
|
+
const store = fileStore<{ id: string; n: number }>('saves')
|
|
82
|
+
await store.write({ id: 'a', n: 1 })
|
|
83
|
+
files.set('saves/broken', '{ cut sho')
|
|
84
|
+
expect(await store.read('a')).toEqual({ id: 'a', n: 1 })
|
|
85
|
+
expect(await store.read('broken')).toBeUndefined()
|
|
86
|
+
expect(await store.list()).toEqual([{ id: 'a', n: 1 }])
|
|
87
|
+
await store.remove('a')
|
|
88
|
+
expect(await store.read('a')).toBeUndefined()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('saves as the window closes and only then lets it close, and not for longer than the limit', async () => {
|
|
92
|
+
const { calls, listeners } = pretendBridge()
|
|
93
|
+
let saved = false
|
|
94
|
+
const client = {
|
|
95
|
+
saves: {
|
|
96
|
+
canSave: () => true,
|
|
97
|
+
refresh: async () => undefined,
|
|
98
|
+
list: () => [],
|
|
99
|
+
save: async () => ((saved = true), { id: 's', name: 'Autosave' }),
|
|
100
|
+
remove: async () => undefined
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await autosaveOnClose(() => client as never)
|
|
104
|
+
await listeners.close[0]!()
|
|
105
|
+
expect(saved).toBe(true)
|
|
106
|
+
expect(calls).toEqual([['destroy']])
|
|
107
|
+
|
|
108
|
+
const hung = pretendBridge()
|
|
109
|
+
vi.useFakeTimers()
|
|
110
|
+
await autosaveOnClose(() => ({ saves: { ...client.saves, save: () => new Promise(() => undefined) } }) as never, { limit: 50 })
|
|
111
|
+
const closing = hung.listeners.close[0]!() as unknown as Promise<void>
|
|
112
|
+
await vi.advanceTimersByTimeAsync(60)
|
|
113
|
+
await closing
|
|
114
|
+
vi.useRealTimers()
|
|
115
|
+
expect(hung.calls).toEqual([['destroy']])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('keeps the Windowed setting to the window', async () => {
|
|
119
|
+
pretendBridge()
|
|
120
|
+
const values = new Map<string, boolean>([['windowed', true]])
|
|
121
|
+
const client = { settings: { get: (id: string) => values.get(id), set: (id: string, on: boolean) => values.set(id, on) } }
|
|
122
|
+
let heard = () => undefined as unknown
|
|
123
|
+
await watchWindowed(client as never, { onResized: async (handler) => ((heard = handler), () => undefined) }, async () => true)
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
125
|
+
vi.useFakeTimers({ now: Date.now() + 5000 })
|
|
126
|
+
await heard()
|
|
127
|
+
vi.useRealTimers()
|
|
128
|
+
expect(values.get('windowed')).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('writes the error log in order, and a failing log does not throw', async () => {
|
|
132
|
+
const lines: string[] = []
|
|
133
|
+
const log = createErrorLog(
|
|
134
|
+
{ append: async (_file, text) => void lines.push(text), size: async () => 0, rename: async () => undefined },
|
|
135
|
+
{ now: () => new Date(0) }
|
|
136
|
+
)
|
|
137
|
+
await log.write('here', 'it broke')
|
|
138
|
+
expect(lines).toEqual(['1970-01-01T00:00:00.000Z [here] it broke\n'])
|
|
139
|
+
await createErrorLog({ append: () => Promise.reject(new Error('no')), size: async () => 0, rename: async () => undefined }).write('x', 'y')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('offers a version the shell found, and installs it by the shell', async () => {
|
|
143
|
+
const { calls, found } = pretendBridge()
|
|
144
|
+
expect(await checkForUpdate()).toBeUndefined()
|
|
145
|
+
found({ version: '1.2.0', notes: 'What is new' })
|
|
146
|
+
const update = await checkForUpdate()
|
|
147
|
+
expect(update).toMatchObject({ version: '1.2.0', notes: 'What is new' })
|
|
148
|
+
await update!.install()
|
|
149
|
+
expect(calls).toEqual([['check'], ['check'], ['install']])
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
// @end
|
package/layers/rooms/server.ts
CHANGED
|
@@ -7,11 +7,17 @@ import { GameRooms, generatedMode, mapMode, type GameMode, type RoomKind } from
|
|
|
7
7
|
// @if ratings
|
|
8
8
|
import { createRatings } from '@v12-engine/engine/server'
|
|
9
9
|
// @end
|
|
10
|
-
import {
|
|
11
|
-
import { rules, start } from './src/rules'
|
|
10
|
+
import { game } from './src/game'
|
|
12
11
|
|
|
13
|
-
/**
|
|
14
|
-
const
|
|
12
|
+
/** The game's rules: a server reads the game's (`src/game.ts`), never its look */
|
|
13
|
+
const rules = game.toRules()
|
|
14
|
+
/** The game's maps by name, each read */
|
|
15
|
+
const maps = Object.fromEntries(await Promise.all(Object.keys(game.toMaps()).map(async (name) => [name, await game.loadMap(name)] as const)))
|
|
16
|
+
/** The first: the room to try the game alone in plays it */
|
|
17
|
+
const DEFAULT_MAP = Object.keys(maps)[0]
|
|
18
|
+
|
|
19
|
+
/** A game on one of the game's maps, each world started as the page starts it (`game.onStart`) */
|
|
20
|
+
const onMap = (name: string): GameMode => mapMode(game, maps[name])
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* A game on a map the game's generator makes (`rules.generator`): another for each game made, of 128 cells a side for
|
|
@@ -22,13 +28,12 @@ const random: Record<string, RoomKind> = rules.generator
|
|
|
22
28
|
random: {
|
|
23
29
|
name: 'A random map',
|
|
24
30
|
room: (settings) => ({
|
|
25
|
-
mode: generatedMode(
|
|
31
|
+
mode: generatedMode(game, rules.generator!, {
|
|
26
32
|
width: 128,
|
|
27
33
|
depth: 128,
|
|
28
34
|
players: 2,
|
|
29
35
|
symmetry: 'half',
|
|
30
|
-
...(Number(settings.seed) > 0 && { seed: Number(settings.seed) })
|
|
31
|
-
start
|
|
36
|
+
...(Number(settings.seed) > 0 && { seed: Number(settings.seed) })
|
|
32
37
|
})
|
|
33
38
|
})
|
|
34
39
|
}
|
|
@@ -42,9 +47,7 @@ const ratings = createRatings({ file: 'ratings.json' })
|
|
|
42
47
|
await ratings.ready
|
|
43
48
|
// @end
|
|
44
49
|
// the seats the computer plays in the room to try the game alone in: every player of the first map but the first
|
|
45
|
-
const computer =
|
|
46
|
-
.players.slice(1)
|
|
47
|
-
.map((player) => player.id)
|
|
50
|
+
const computer = maps[DEFAULT_MAP].players.slice(1).map((player) => player.id)
|
|
48
51
|
const rooms = await GameRooms.start({
|
|
49
52
|
port: Number(process.env.PORT ?? 8787),
|
|
50
53
|
rooms: {
|
package/layers/server/server.ts
CHANGED
|
@@ -7,10 +7,9 @@ import { startServer } from '@v12-engine/engine/server'
|
|
|
7
7
|
// @if ratings
|
|
8
8
|
import { createRatings } from '@v12-engine/engine/server'
|
|
9
9
|
// @end
|
|
10
|
-
import {
|
|
11
|
-
import { rules, start } from './src/rules'
|
|
10
|
+
import { game } from './src/game'
|
|
12
11
|
|
|
13
|
-
const map =
|
|
12
|
+
const map = await game.loadMap()
|
|
14
13
|
// @if ratings
|
|
15
14
|
// a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
|
|
16
15
|
const ratings = createRatings({ file: 'ratings.json' })
|
|
@@ -18,9 +17,9 @@ const ratings = createRatings({ file: 'ratings.json' })
|
|
|
18
17
|
await ratings.ready
|
|
19
18
|
// @end
|
|
20
19
|
const server = await startServer({
|
|
21
|
-
game: rules,
|
|
20
|
+
// the game: its rules and its start, never its look (`src/game.ts`), on its first map
|
|
21
|
+
game,
|
|
22
22
|
map,
|
|
23
|
-
start,
|
|
24
23
|
// the seats the computer plays: leave it out for a game of people alone, which starts once every seat is taken
|
|
25
24
|
ai: map.players.slice(1).map((player) => player.id),
|
|
26
25
|
// @if ratings
|
|
@@ -34,11 +34,31 @@ export default defineConfig({
|
|
|
34
34
|
assetsPackage(),
|
|
35
35
|
// @end
|
|
36
36
|
// @if editor
|
|
37
|
-
editorFiles({
|
|
37
|
+
editorFiles({
|
|
38
|
+
gameFolder: () => 'src',
|
|
39
|
+
// where New with code in the editor may write a stub (never over a file)
|
|
40
|
+
scaffold: ['src'],
|
|
41
|
+
// the scripts the editor's Run panel may start and stop while developing, and no others
|
|
42
|
+
run: [
|
|
43
|
+
// @if server
|
|
44
|
+
'server',
|
|
45
|
+
// @end
|
|
46
|
+
// @if desktop
|
|
47
|
+
// (`app` starts the dev server itself, which the editor is open on: this one uses that one)
|
|
48
|
+
'app:editor',
|
|
49
|
+
// @end
|
|
50
|
+
]
|
|
51
|
+
})
|
|
38
52
|
// @end
|
|
39
53
|
],
|
|
40
54
|
// the packages' source on the page, on the dev server's own side and in the tests (the library's `config/vite.ts`)
|
|
41
55
|
...sourceLibrary(),
|
|
56
|
+
// @if desktop
|
|
57
|
+
// the app made to hand out is not the page's to watch
|
|
58
|
+
server: { watch: { ignored: ['**/out/**'] } },
|
|
59
|
+
// the shell's own Chromium (Electron's) is the one browser the built page runs in
|
|
60
|
+
build: { target: 'esnext' },
|
|
61
|
+
// @end
|
|
42
62
|
// @if tests
|
|
43
63
|
test: { environment: 'node' }
|
|
44
64
|
// @end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v12-engine/create",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"description": "A project to start a game from: npm create @v12-engine my-game",
|
|
15
15
|
"bin": {
|
|
16
|
+
"create": "./bin.mjs",
|
|
16
17
|
"create-v12": "./bin.mjs",
|
|
17
18
|
"v12": "./v12.mjs"
|
|
18
19
|
},
|
|
@@ -22,12 +22,16 @@ A campaign made with V12: three missions on the long road home, a hero carried f
|
|
|
22
22
|
<!-- @end -->
|
|
23
23
|
| Check the types | `npm run typecheck` |
|
|
24
24
|
| Build the page to host | `npm run build`, into `dist/` |
|
|
25
|
+
<!-- @if desktop -->
|
|
26
|
+
| Play it as an app | `npm run app`: the page in a window of its own (Electron, fetched the first time) |
|
|
27
|
+
| Make the app to hand out | `npm run package`, into `out/` |
|
|
28
|
+
<!-- @end -->
|
|
25
29
|
|
|
26
30
|
## What is where
|
|
27
31
|
|
|
28
32
|
- `src/campaign/`: the game. `index.ts` puts the rules together and gives each map its mission; `ids.ts` names everything once; `units.ts` the units, buildings, the hero, his abilities and the items; `missions/` a file a mission: `homecoming.ts` (objectives, a prefab, areas, a brain that holds and raids), `siege.ts` (the waves, the gate, effects fired from the rules), `warlord.ts` (the hero carried in, a shop, the talk at the gate and its choice, the prisoners); `waves.ts` the waves, as `waves.json` says them; `view/` how it looks and sounds, with the cinematic of the talk in code; `cinematics/` the intro and the outro, made in the editor's Cinematic panel; `prefabs/` what the editor's Save as a prefab wrote; `campaign.json` the campaign's chapters and missions (their maps, words, briefings, debriefs), what the editor's Campaign panel writes, and `scenes.ts` what of them is code, for the page (`defineCampaign`); `data.json`, `looks.json`, `fx.json` and `waves.json` what the editor's Data, model, Effects and Waves panels write
|
|
29
|
-
- `src/
|
|
30
|
-
- `src/maps/`: the missions' maps, made in the editor. Each has the names its mission's rules look for (`hall`, `hero`, `camp`, `warcamp`, `gate`, `cells`…): a names file beside each says which
|
|
33
|
+
- `src/game.ts`: the game (`createGame`) the page, the server, the editor, the tests and the campaign's scenes take: the campaign's rules, its look fetched as the page first plays it, its maps (`src/maps.ts`), and the mission of a map given as a game of it starts
|
|
34
|
+
- `src/maps/`: the missions' maps, made in the editor, each with a meadow (the rules' `grass` layer, `foliagePack`: a meadow that burns and grows back, and on the homestead tall grass that hides). Each has the names its mission's rules look for (`hall`, `hero`, `camp`, `warcamp`, `gate`, `cells`…): a names file beside each says which
|
|
31
35
|
- `src/main.ts`: the page: the start screen, the campaign's menu and its missions, free play, the settings
|
|
32
36
|
|
|
33
37
|
<!-- @if editor -->
|
|
@@ -48,6 +52,9 @@ A campaign made with V12: three missions on the long road home, a hero carried f
|
|
|
48
52
|
|
|
49
53
|
- `tests/`: the missions tested with no page: the maps checked, each mission won and lost as it says, the waves, the carried hero, the choice at the gate, the computer playing a minute (`simulate`)
|
|
50
54
|
<!-- @end -->
|
|
55
|
+
<!-- @if desktop -->
|
|
56
|
+
- `electron/`: the shell around the page, Electron's main process: `config.mjs` (the app's name, its identifier and the updater's address, yours to settle before it is handed out), `app.mjs` (the window, its full screen, what the page may ask), `preload.cjs` (the bridge the page asks through), `files.mjs` (saves, settings and the error log as files), `updates.mjs`, `presentation.m` (the addon that hides the Dock and the menu bar on macOS), `dev.mjs` (`npm run app`) and `package.mjs` (`npm run package`). `src/shell.ts` is all the page asks of it and `src/desktop.ts` what it does with that: over the whole screen with a Windowed setting, saves and settings as files, the game saved as the window closes, an error log, Exit on the start menu, Quit on the pause menu. The updater's address (`UPDATES`) is a placeholder until the docs' A desktop game, offline says what goes there, whose example is this campaign in such a shell
|
|
57
|
+
<!-- @end -->
|
|
51
58
|
- `public/games/campaign/`: the game's own folder of files: what the editor's Assets panel takes in (models, sounds, pictures), with an `assets.json` saying what is there
|
|
52
59
|
- `@v12-engine/assets`: the shared art every unit and building is drawn with (adventurers with their clips and their gear, buildings, trees), served where `public/`'s files are by `assetsPackage()` in `vite.config.ts`, and copied into a build as the game uses it. Whose each file is: its group's `CREDITS.md` in `node_modules/@v12-engine/assets/groups/`
|
|
53
60
|
|
|
@@ -7,11 +7,11 @@ import { GameRooms, mapMode, type GameMode } from '@v12-engine/engine/server'
|
|
|
7
7
|
// @if ratings
|
|
8
8
|
import { createRatings } from '@v12-engine/engine/server'
|
|
9
9
|
// @end
|
|
10
|
+
import { game, PLAYER } from './src/game'
|
|
10
11
|
import { mapNamed, maps } from './src/maps'
|
|
11
|
-
import { PLAYER, rules, start } from './src/rules'
|
|
12
12
|
|
|
13
|
-
/** A game on one of the missions' maps: the one seat is Aldric's, the Red Hand the mission's own computer, which
|
|
14
|
-
const onMap = (name: string): GameMode => mapMode(
|
|
13
|
+
/** A game on one of the missions' maps: the one seat is Aldric's, the Red Hand the mission's own computer, which its start seats */
|
|
14
|
+
const onMap = (name: string): GameMode => mapMode(game, mapNamed(name), { seats: [{ player: PLAYER, name: 'Aldric' }], ai: null })
|
|
15
15
|
|
|
16
16
|
// @if ratings
|
|
17
17
|
// a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
|
|
@@ -6,8 +6,7 @@ import { startServer } from '@v12-engine/engine/server'
|
|
|
6
6
|
// @if ratings
|
|
7
7
|
import { createRatings } from '@v12-engine/engine/server'
|
|
8
8
|
// @end
|
|
9
|
-
import {
|
|
10
|
-
import { PLAYER, rules, start } from './src/rules'
|
|
9
|
+
import { game, PLAYER } from './src/game'
|
|
11
10
|
|
|
12
11
|
// @if ratings
|
|
13
12
|
// a rating for each player, moved by every game that ends with a winner, kept in a file (`ratings.json`)
|
|
@@ -16,9 +15,8 @@ const ratings = createRatings({ file: 'ratings.json' })
|
|
|
16
15
|
await ratings.ready
|
|
17
16
|
// @end
|
|
18
17
|
const server = await startServer({
|
|
19
|
-
game: rules,
|
|
20
|
-
|
|
21
|
-
start,
|
|
18
|
+
// the game: its rules and its start (the mission of its map), never its look (`src/game.ts`), on its first map
|
|
19
|
+
game,
|
|
22
20
|
// the one seat is Aldric's: the Red Hand is the mission's own computer, which `start` seats
|
|
23
21
|
seats: [{ player: PLAYER, name: 'Aldric' }],
|
|
24
22
|
computer: null,
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
newCampaignProgress,
|
|
15
15
|
readCampaignProgress,
|
|
16
16
|
type World,
|
|
17
|
-
heroOf
|
|
17
|
+
heroOf,
|
|
18
|
+
heroCallings
|
|
18
19
|
} from '@v12-engine/engine'
|
|
19
20
|
import { checkDeterminism, order, run, simulate, testWorld } from '@v12-engine/engine/testing'
|
|
20
21
|
import { describe, expect, it } from 'vitest'
|
|
@@ -23,16 +24,19 @@ import { kills } from '../src/campaign/kills'
|
|
|
23
24
|
import { builtWaves, FIRST_WAIT, readWaves, siegeWaves, wavesOf } from '../src/campaign/waves'
|
|
24
25
|
import { warlord } from '../src/campaign/missions/warlord'
|
|
25
26
|
import { mapNamed, maps } from '../src/maps'
|
|
26
|
-
import { ENEMY,
|
|
27
|
+
import { ENEMY, game, PLAYER, UNIT } from '../src/game'
|
|
27
28
|
import wavesFile from '../src/campaign/waves.json'
|
|
28
29
|
|
|
30
|
+
const rules = game.toRules()
|
|
29
31
|
const registry = createRegistry(rules)
|
|
32
|
+
/** What the game does as a world of it starts: the mission of its map (`game.onStart`), at once */
|
|
33
|
+
const onStart = game.toStart()!
|
|
30
34
|
|
|
31
|
-
/** A mission's world, started as the page starts it */
|
|
35
|
+
/** A mission's world, started as the page starts it (`game.onStart`: the mission of its map) */
|
|
32
36
|
async function mission(name: string): Promise<World> {
|
|
33
37
|
const map = mapNamed(name)
|
|
34
|
-
const world = await createWorldFromMap({ map,
|
|
35
|
-
|
|
38
|
+
const world = await createWorldFromMap({ map, rules, seed: 1 })
|
|
39
|
+
await onStart(world, { map })
|
|
36
40
|
return world
|
|
37
41
|
}
|
|
38
42
|
|
|
@@ -95,7 +99,7 @@ describe('mission one, Homecoming', () => {
|
|
|
95
99
|
it('plays, the Red Hand’s computer thinking, with no error for a minute (simulate)', async () => {
|
|
96
100
|
const map = mapNamed('homestead')
|
|
97
101
|
// the mission seats the Red Hand's computer itself (`start`): simulate is told of none
|
|
98
|
-
const [game] = await simulate({
|
|
102
|
+
const [game] = await simulate({ rules: rules, map, computers: [], seconds: 60, onStart: (world) => void onStart(world, { map }) }, { seeds: [1] })
|
|
99
103
|
expect(game.failed).toBeUndefined()
|
|
100
104
|
expect(game.ticks).toBeGreaterThan(0)
|
|
101
105
|
})
|
|
@@ -169,7 +173,7 @@ describe('what goes on to the next mission', () => {
|
|
|
169
173
|
const footmen = standing(home, PLAYER, UNIT.footman)
|
|
170
174
|
const more = [home.spawn(UNIT.footman, PLAYER, { x: 20, z: 20 }), home.spawn(UNIT.footman, PLAYER, { x: 22, z: 20 })]
|
|
171
175
|
const hurt = more[0]
|
|
172
|
-
hurt.
|
|
176
|
+
hurt.health = 10
|
|
173
177
|
home.objectives.complete(PLAYER, 'campaign.caravan')
|
|
174
178
|
const carried = homecomingCarry(home)
|
|
175
179
|
expect(footmen.length + more.length).toBe(4)
|
|
@@ -182,8 +186,8 @@ describe('what goes on to the next mission', () => {
|
|
|
182
186
|
home.dispose()
|
|
183
187
|
|
|
184
188
|
const map = mapNamed('fort')
|
|
185
|
-
const fort = await createWorldFromMap({ map,
|
|
186
|
-
|
|
189
|
+
const fort = await createWorldFromMap({ map, rules, seed: 1 })
|
|
190
|
+
onStart(fort, { map })
|
|
187
191
|
const before = { footmen: standing(fort, PLAYER, UNIT.footman).length, gold: fort.player(PLAYER).resources.gold ?? 0 }
|
|
188
192
|
siegeCarriedIn(fort, map, kept.data as Partial<RoadData>)
|
|
189
193
|
expect(standing(fort, PLAYER, UNIT.footman)).toHaveLength(before.footmen + 3)
|
|
@@ -206,9 +210,9 @@ describe('mission three, The Warlord', () => {
|
|
|
206
210
|
|
|
207
211
|
it('brings the carried hero in where the map has one, with what he earned', async () => {
|
|
208
212
|
const map = mapNamed('pass')
|
|
209
|
-
const world = await createWorldFromMap({ map,
|
|
213
|
+
const world = await createWorldFromMap({ map, rules, seed: 1 })
|
|
210
214
|
const [hero] = carryIn(world, [seasoned], PLAYER, map.players[0].start!)
|
|
211
|
-
|
|
215
|
+
onStart(world, { map })
|
|
212
216
|
expect(heroOf(hero)?.level).toBe(3)
|
|
213
217
|
expect(hero.abilities.find((ability) => ability.id === 'holyLight')?.level).toBe(2)
|
|
214
218
|
expect(standing(world, PLAYER, UNIT.knight)).toHaveLength(1)
|
|
@@ -218,7 +222,7 @@ describe('mission three, The Warlord', () => {
|
|
|
218
222
|
it('is harder on hard and easier on easy, its computer seated on the campaign’s difficulty', async () => {
|
|
219
223
|
const on = async (difficulty: string) => {
|
|
220
224
|
const map = mapNamed('pass')
|
|
221
|
-
const world = await createWorldFromMap({ map,
|
|
225
|
+
const world = await createWorldFromMap({ map, rules, seed: 1 })
|
|
222
226
|
// what the campaign's scene does before a mission seats its computers
|
|
223
227
|
world.computers.defaults = { difficulty }
|
|
224
228
|
warlord(world, true, difficulty)
|
|
@@ -229,7 +233,7 @@ describe('mission three, The Warlord', () => {
|
|
|
229
233
|
expect((hard.computers.get(ENEMY)?.computer as { profile?: { difficulty?: string } }).profile?.difficulty).toBe('hard')
|
|
230
234
|
const easy = await on('easy')
|
|
231
235
|
const varga = easy.named('warlord')!
|
|
232
|
-
expect(varga.
|
|
236
|
+
expect(varga.health).toBe(Math.round(varga.maxHealth * 0.75))
|
|
233
237
|
expect(easy.named('guard4')).toBeUndefined()
|
|
234
238
|
hard.dispose()
|
|
235
239
|
easy.dispose()
|
|
@@ -271,15 +275,30 @@ describe('mission three, The Warlord', () => {
|
|
|
271
275
|
})
|
|
272
276
|
|
|
273
277
|
describe('the hero', () => {
|
|
278
|
+
it('is a Vanguard and a Keeper, of the engine’s callings', () => {
|
|
279
|
+
expect(heroCallings(registry, registry.unit(UNIT.knight)).map(({ calling, strength }) => [calling.name, strength])).toEqual([
|
|
280
|
+
['Vanguard', 3],
|
|
281
|
+
['Keeper', 2]
|
|
282
|
+
])
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('brings two of the engine’s hero spells, Barrier and Purify, and the shop names his items’ tags', () => {
|
|
286
|
+
expect(registry.unit(UNIT.knight).hero!.spells).toEqual(['barrier', 'purify'])
|
|
287
|
+
expect(['barrier', 'purify'].map((id) => registry.ability(id).name)).toEqual(['Barrier', 'Purify'])
|
|
288
|
+
// of the set, only what he brings
|
|
289
|
+
expect(rules.abilities!.some((ability) => ability.id === 'flash')).toBe(false)
|
|
290
|
+
expect(rules.tags!.map((tag) => tag.id).sort()).toEqual(['attack', 'consumable', 'health', 'movement'])
|
|
291
|
+
})
|
|
292
|
+
|
|
274
293
|
it('heals a friend with Holy Light once he has learned it', async () => {
|
|
275
294
|
const world = await testWorld(rules)
|
|
276
295
|
const hero = world.spawn(UNIT.knight, PLAYER, { x: 20, z: 20 })
|
|
277
296
|
const footman = world.spawn(UNIT.footman, PLAYER, { x: 22, z: 20 })
|
|
278
297
|
learn(world, hero, 'holyLight')
|
|
279
|
-
footman.
|
|
298
|
+
footman.health = 100
|
|
280
299
|
order(world, [hero], { type: 'cast', ability: 'holyLight', target: footman.id })
|
|
281
|
-
run(world, 3, () => footman.
|
|
282
|
-
expect(footman.
|
|
300
|
+
run(world, 3, () => footman.health > 200)
|
|
301
|
+
expect(footman.health).toBeGreaterThan(200)
|
|
283
302
|
world.dispose()
|
|
284
303
|
})
|
|
285
304
|
|
|
@@ -289,13 +308,13 @@ describe('the hero', () => {
|
|
|
289
308
|
// something of the enemy's that does not hit back: what the strike took is then the strike's alone
|
|
290
309
|
const captive = world.spawn(UNIT.prisoner, ENEMY, { x: 23, z: 20 })
|
|
291
310
|
learn(world, hero, 'judgement')
|
|
292
|
-
hero.
|
|
311
|
+
hero.health = 300
|
|
293
312
|
// a tick first: an enemy is a target once it is seen, and the fog is worked out as the game steps
|
|
294
313
|
run(world, 0.1)
|
|
295
314
|
order(world, [hero], { type: 'cast', ability: 'judgement', target: captive.id })
|
|
296
|
-
run(world, 3, () => captive.
|
|
297
|
-
expect(captive.
|
|
298
|
-
expect(hero.
|
|
315
|
+
run(world, 3, () => captive.health <= captive.maxHealth - 120)
|
|
316
|
+
expect(captive.maxHealth - captive.health).toBeGreaterThanOrEqual(120)
|
|
317
|
+
expect(hero.health).toBeGreaterThanOrEqual(300 + 120 - 1)
|
|
299
318
|
world.dispose()
|
|
300
319
|
})
|
|
301
320
|
})
|
|
@@ -312,7 +331,7 @@ describe('each mission', () => {
|
|
|
312
331
|
it.each(Object.keys(maps))(
|
|
313
332
|
'%s comes out the same, played twice and on from a save',
|
|
314
333
|
async (name) => {
|
|
315
|
-
const report = await checkDeterminism(() => mission(name), { ticks: 600, from: 150,
|
|
334
|
+
const report = await checkDeterminism(() => mission(name), { ticks: 600, from: 150, rules })
|
|
316
335
|
expect(report).toEqual({ twice: null, fromSave: null })
|
|
317
336
|
},
|
|
318
337
|
120_000
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { campaign } from './index'
|
|
3
|
-
import {
|
|
1
|
+
import { createGame } from '@v12-engine/engine'
|
|
2
|
+
import { campaign, startMission } from './index'
|
|
3
|
+
import { maps } from './maps'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The campaign's game, whole (`createGame`): its rules, its missions' maps, the mission of a map given as a game of it
|
|
7
|
+
* starts, and its look, fetched as a page first plays it (`./view`, and the client that draws it: not part of a start
|
|
8
|
+
* screen's code, nor of what a server or a test reads). What a page launches, its editor makes maps for and its
|
|
9
|
+
* campaign's scenes play (`scenes.ts`)
|
|
10
|
+
*/
|
|
11
|
+
export const campaignGame = createGame(campaign).maps(maps)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What the game does once a world is made from a map, before anyone plays: the map's mission is given (each map is
|
|
15
|
+
* a mission's; a map of none plays on with no objectives). The page's free play, the server, the tests and the
|
|
16
|
+
* editor's Play here each run it with the map; the campaign's own scene gives the mission itself, knowing whether
|
|
17
|
+
* the game is new or loaded from a save
|
|
18
|
+
*/
|
|
19
|
+
campaignGame.onStart((world, { map }) => startMission(world, map))
|
|
20
|
+
|
|
21
|
+
// @if !desktop
|
|
22
|
+
campaignGame.view(() => import('./view').then((module) => module.campaignView))
|
|
23
|
+
// @end
|
|
24
|
+
// @if desktop
|
|
25
|
+
// its look with what the desktop app adds to it: a Windowed setting, Quit on the pause menu
|
|
26
|
+
campaignGame.view(() => Promise.all([import('./view'), import('../desktop')]).then(([{ campaignView }, { desktopView }]) => desktopView(campaignView)))
|
|
27
|
+
// @end
|