@v12-engine/create 0.1.1 → 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 +1 -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,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RTS examples on the client's side: how their units and buildings look (the shared art's characters, KayKit's,
|
|
3
|
+
* and its buildings, Quaternius's), the HUD's chips for gold and lumber, and each example's controls (`CategoryLooks`)
|
|
4
|
+
*/
|
|
5
|
+
import sizesByPath from '@v12-engine/assets/sizes.json'
|
|
6
|
+
import { type AbilityLooks, type GameClientApi, type ModelDef, presetFx, type UnitLook } from '@v12-engine/client'
|
|
7
|
+
import { cargo, snapshotOf } from '@v12-engine/engine'
|
|
8
|
+
import { button, readout, type CategoryLooks } from '../../shell/look'
|
|
9
|
+
import { character, hold, ONE_HANDED, person, TWO_HANDED } from '../common/looks'
|
|
10
|
+
import { ENEMY, PLAYER, UNIT } from '../common/units'
|
|
11
|
+
import { attackNow, HOME, resetAi, THEIRS } from './ai'
|
|
12
|
+
import { backToWork, research, train } from './base'
|
|
13
|
+
import { build, resetBuild } from './build'
|
|
14
|
+
import { CARRY, carry } from './carry'
|
|
15
|
+
import { attackCamp, pullBack, resetCreeps } from './creeps'
|
|
16
|
+
import { RTS } from './data'
|
|
17
|
+
import { resetAirlift } from './airlift'
|
|
18
|
+
import { resetFerry } from './ferry'
|
|
19
|
+
import { gnollsCome, resetGarrison, TOWER } from './garrison'
|
|
20
|
+
import { giveXp, hunt, learnSpell, resetHero } from './hero'
|
|
21
|
+
import { buy, resetShop } from './shop'
|
|
22
|
+
import { toKeep } from './tech'
|
|
23
|
+
import { rally, resetTrain, smithy } from './train'
|
|
24
|
+
import { edgeAt, riverAt } from './waterside'
|
|
25
|
+
|
|
26
|
+
const STONE: [number, number, number] = [0.66, 0.63, 0.58]
|
|
27
|
+
const WOOD: [number, number, number] = [0.45, 0.31, 0.18]
|
|
28
|
+
const GOLD: [number, number, number] = [1, 0.8, 0.2]
|
|
29
|
+
const STRAW: [number, number, number] = [0.8, 0.66, 0.32]
|
|
30
|
+
|
|
31
|
+
/** Every model of the shared art by its path, with its size in its own units: across, up, deep */
|
|
32
|
+
const sizes = sizesByPath as Record<string, number[]>
|
|
33
|
+
|
|
34
|
+
/** A building of the shared art made `width` across at its widest, a lamp over its door (the packs' buildings face +z) */
|
|
35
|
+
function building(src: string, width: number): ModelDef {
|
|
36
|
+
const [across = 1, up = 1, deep = 1] = sizes[src] ?? []
|
|
37
|
+
return {
|
|
38
|
+
src,
|
|
39
|
+
scale: width / Math.max(across, deep),
|
|
40
|
+
lights: [{ name: 'door', offset: [0, up * 0.3, deep / 2 + 0.12], color: [1, 0.74, 0.45], intensity: 1.3, range: 5, flicker: 0.06 }],
|
|
41
|
+
// over its roof: where it smokes and burns when hurt
|
|
42
|
+
points: { roof: { offset: [0, up * 0.8, 0] } }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A building's look: its model, its parts while that loads, smoke and fire as it is hurt, rising as it is built, crumbling as it falls */
|
|
47
|
+
const structure = (src: string, width: number, height: number, shapes: UnitLook['shapes']): UnitLook => ({
|
|
48
|
+
height,
|
|
49
|
+
model: building(src, width),
|
|
50
|
+
shapes,
|
|
51
|
+
damaged: [
|
|
52
|
+
{ below: 0.5, fx: 'smoke', at: 'roof', params: { size: 0.8, darkness: 0.6 } },
|
|
53
|
+
{ below: 0.25, fx: 'fire', at: 'roof', params: { size: 0.9, smoke: false } }
|
|
54
|
+
],
|
|
55
|
+
death: { collapse: { look: 'crumble', pieces: 18, dust: true } },
|
|
56
|
+
// it goes up from the ground with a glowing edge, dust as it is laid out and done, sparks off the hammers
|
|
57
|
+
construction: {
|
|
58
|
+
motion: 'reveal',
|
|
59
|
+
stages: [{ at: 0, fx: 'dust' }],
|
|
60
|
+
working: { fx: 'spark-burst', at: 'handslot.r', params: { count: 8, size: 0.6 } },
|
|
61
|
+
done: { fx: 'dust' }
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/** The trees, by kind: oaks, pines and columnar trees of the Stylized Trees pack, one each in turn */
|
|
66
|
+
const FOREST = ['oak_5', 'pine_1', 'columnar_1', 'columnar_2', 'pine_3', 'pine_4']
|
|
67
|
+
const TREE_LOOKS: Record<string, ModelDef> = Object.fromEntries(
|
|
68
|
+
FOREST.map((kind, i) => [String(i + 1).padStart(3, '0'), { src: `trees/${kind}.glb`, scale: kind.startsWith('oak') ? 0.6 : 1 }])
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const HULL: [number, number, number] = [0.36, 0.22, 0.13]
|
|
72
|
+
const DECK: [number, number, number] = [0.62, 0.45, 0.27]
|
|
73
|
+
const CANVAS: [number, number, number] = [0.93, 0.9, 0.8]
|
|
74
|
+
const IRON: [number, number, number] = [0.25, 0.25, 0.27]
|
|
75
|
+
/** How far the ship's sails are swung off square, as they are trimmed to a wind from the side */
|
|
76
|
+
const TRIM = 0.45
|
|
77
|
+
|
|
78
|
+
/** Solids made `k` times as big, and as far from the middle */
|
|
79
|
+
const grown = (k: number, shapes: UnitLook['shapes']): UnitLook['shapes'] =>
|
|
80
|
+
shapes.map((each) => ({
|
|
81
|
+
...each,
|
|
82
|
+
size: [each.size[0] * k, each.size[1] * k, each.size[2] * k],
|
|
83
|
+
offset: [each.offset[0] * k, each.offset[1] * k, each.offset[2] * k]
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The transport ship, built of solids, its bow to +z: a rounded hull half under the water, a deck and rails, a raised
|
|
88
|
+
* stern with its cabin, a mast with two sails (the lower in canvas, the upper in its side's colour), a bowsprit and a
|
|
89
|
+
* pennant. The shared art has no ship: a real model would be better
|
|
90
|
+
*/
|
|
91
|
+
const SHIP: UnitLook['shapes'] = grown(1.25, [
|
|
92
|
+
// the hull: a long rounded body, the bow narrowed by a second one forward
|
|
93
|
+
{ shape: 'sphere', size: [2, 1.3, 4.6], offset: [0, 0.2, -0.2], color: HULL },
|
|
94
|
+
{ shape: 'sphere', size: [1.5, 1.1, 2.4], offset: [0, 0.32, 1.5], color: HULL },
|
|
95
|
+
// a stripe of its side's colour along the hull, just over the water
|
|
96
|
+
{ shape: 'box', size: [2.02, 0.14, 3], offset: [0, 0.5, -0.3], color: 'team' },
|
|
97
|
+
// the deck, the rails along it
|
|
98
|
+
{ shape: 'box', size: [1.7, 0.1, 3.6], offset: [0, 0.78, -0.1], color: DECK },
|
|
99
|
+
{ shape: 'box', size: [0.08, 0.3, 3.4], offset: [0.86, 0.95, -0.2], color: HULL },
|
|
100
|
+
{ shape: 'box', size: [0.08, 0.3, 3.4], offset: [-0.86, 0.95, -0.2], color: HULL },
|
|
101
|
+
// the raised stern, its cabin and lantern
|
|
102
|
+
{ shape: 'box', size: [1.76, 0.5, 1.1], offset: [0, 1.05, -1.95], color: HULL },
|
|
103
|
+
{ shape: 'box', size: [1.2, 0.55, 0.8], offset: [0, 1.55, -1.9], color: DECK },
|
|
104
|
+
{ shape: 'pyramid', size: [1.4, 0.4, 1], offset: [0, 2.02, -1.9], color: 'team' },
|
|
105
|
+
{ shape: 'sphere', size: [0.18, 0.24, 0.18], offset: [0, 1.5, -2.55], color: [1, 0.8, 0.4] },
|
|
106
|
+
// the mast, its yards and crow's nest
|
|
107
|
+
{ shape: 'cylinder', size: [0.16, 4.2, 0.16], offset: [0, 2.9, 0.25], color: HULL },
|
|
108
|
+
{ shape: 'cylinder', size: [0.09, 2.3, 0.09], offset: [0, 3.65, 0.3], color: HULL, tilt: Math.PI / 2, turn: Math.PI / 2 + TRIM },
|
|
109
|
+
{ shape: 'cylinder', size: [0.08, 1.6, 0.08], offset: [0, 4.55, 0.3], color: HULL, tilt: Math.PI / 2, turn: Math.PI / 2 + TRIM },
|
|
110
|
+
{ shape: 'cylinder', size: [0.42, 0.22, 0.42], offset: [0, 4.95, 0.25], color: DECK },
|
|
111
|
+
// the sails: the big one in canvas, bellied forward a little; the top one in its side's colour
|
|
112
|
+
{ shape: 'box', size: [2.2, 1.9, 0.06], offset: [0, 2.68, 0.4], color: CANVAS, tilt: -0.08, turn: TRIM },
|
|
113
|
+
{ shape: 'box', size: [1.5, 0.85, 0.06], offset: [0, 4.12, 0.36], color: 'team', tilt: -0.06, turn: TRIM },
|
|
114
|
+
// the bowsprit, and a jib from it to the mast
|
|
115
|
+
{ shape: 'cylinder', size: [0.1, 1.7, 0.1], offset: [0, 1.05, 2.75], color: HULL, tilt: 1.15 },
|
|
116
|
+
{ shape: 'box', size: [0.05, 1.4, 1.2], offset: [0, 1.85, 1.85], color: CANVAS, tilt: -0.55 },
|
|
117
|
+
// a pennant at the masthead, and a few crates on deck
|
|
118
|
+
{ shape: 'box', size: [0.04, 0.24, 0.6], offset: [0, 5.2, -0.05], color: 'team' },
|
|
119
|
+
{ shape: 'box', size: [0.45, 0.4, 0.45], offset: [0.45, 1.03, -0.85], color: [0.55, 0.4, 0.22] },
|
|
120
|
+
{ shape: 'box', size: [0.4, 0.35, 0.4], offset: [-0.5, 1.0, 1.05], color: [0.55, 0.4, 0.22] },
|
|
121
|
+
{ shape: 'cylinder', size: [0.36, 0.45, 0.36], offset: [-0.45, 1.05, -0.85], color: [0.45, 0.3, 0.16] }
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The zeppelin, built of solids, its nose to +z: a long canvas envelope ringed with bands of its side's colour, tail
|
|
126
|
+
* fins, a wooden gondola slung under it on struts, two engines with propellers either side. The shared art has no
|
|
127
|
+
* airship: a real model would be better
|
|
128
|
+
*/
|
|
129
|
+
const ZEPPELIN: UnitLook['shapes'] = [
|
|
130
|
+
// the envelope, its nose and tail rounded by smaller ones
|
|
131
|
+
{ shape: 'sphere', size: [2.3, 2.1, 5.4], offset: [0, 2.9, 0], color: CANVAS },
|
|
132
|
+
{ shape: 'cylinder', size: [2.36, 0.3, 2.16], offset: [0, 2.9, 1.2], color: 'team', tilt: Math.PI / 2 },
|
|
133
|
+
{ shape: 'cylinder', size: [2.36, 0.3, 2.16], offset: [0, 2.9, -1.2], color: 'team', tilt: Math.PI / 2 },
|
|
134
|
+
{ shape: 'sphere', size: [0.5, 0.5, 0.4], offset: [0, 2.9, 2.62], color: IRON },
|
|
135
|
+
// the tail fins: up, down and either side
|
|
136
|
+
{ shape: 'box', size: [0.07, 1.1, 1.1], offset: [0, 4.05, -2.3], color: 'team', tilt: -0.3 },
|
|
137
|
+
{ shape: 'box', size: [0.07, 0.9, 1], offset: [0, 1.85, -2.3], color: 'team', tilt: 0.3 },
|
|
138
|
+
{ shape: 'box', size: [3, 0.07, 1], offset: [0, 2.9, -2.35], color: 'team' },
|
|
139
|
+
// the gondola under it, its windows and the struts it hangs from
|
|
140
|
+
{ shape: 'box', size: [0.95, 0.6, 2.2], offset: [0, 1.05, 0.25], color: HULL },
|
|
141
|
+
{ shape: 'box', size: [1.02, 0.12, 2.3], offset: [0, 1.4, 0.25], color: DECK },
|
|
142
|
+
{ shape: 'box', size: [0.97, 0.18, 1.6], offset: [0, 1.15, 0.35], color: [1, 0.85, 0.5] },
|
|
143
|
+
{ shape: 'sphere', size: [0.95, 0.6, 0.7], offset: [0, 1.05, 1.32], color: HULL },
|
|
144
|
+
{ shape: 'cylinder', size: [0.06, 0.8, 0.06], offset: [0.4, 1.75, 1], color: IRON, tilt: -0.2 },
|
|
145
|
+
{ shape: 'cylinder', size: [0.06, 0.8, 0.06], offset: [-0.4, 1.75, 1], color: IRON, tilt: -0.2 },
|
|
146
|
+
{ shape: 'cylinder', size: [0.06, 0.8, 0.06], offset: [0.4, 1.75, -0.6], color: IRON, tilt: 0.2 },
|
|
147
|
+
{ shape: 'cylinder', size: [0.06, 0.8, 0.06], offset: [-0.4, 1.75, -0.6], color: IRON, tilt: 0.2 },
|
|
148
|
+
// the engines on outriggers, their propellers behind
|
|
149
|
+
{ shape: 'cylinder', size: [0.07, 1.6, 0.07], offset: [0, 1.6, -0.5], color: IRON, tilt: Math.PI / 2, turn: Math.PI / 2 },
|
|
150
|
+
{ shape: 'cylinder', size: [0.3, 0.6, 0.3], offset: [0.85, 1.6, -0.5], color: IRON, tilt: Math.PI / 2 },
|
|
151
|
+
{ shape: 'cylinder', size: [0.3, 0.6, 0.3], offset: [-0.85, 1.6, -0.5], color: IRON, tilt: Math.PI / 2 },
|
|
152
|
+
{ shape: 'box', size: [0.9, 0.08, 0.04], offset: [0.85, 1.6, -0.85], color: DECK, turn: 0 },
|
|
153
|
+
{ shape: 'box', size: [0.08, 0.9, 0.04], offset: [0.85, 1.6, -0.85], color: DECK },
|
|
154
|
+
{ shape: 'box', size: [0.9, 0.08, 0.04], offset: [-0.85, 1.6, -0.85], color: DECK },
|
|
155
|
+
{ shape: 'box', size: [0.08, 0.9, 0.04], offset: [-0.85, 1.6, -0.85], color: DECK }
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
/** How each unit and building of the RTS examples looks */
|
|
159
|
+
const looks: Record<string, UnitLook> = {
|
|
160
|
+
[RTS.peasant]: {
|
|
161
|
+
height: 1.5,
|
|
162
|
+
model: character('barbarian', 0.58, { hold: [hold('axe_1handed')] }),
|
|
163
|
+
attacks: [ONE_HANDED],
|
|
164
|
+
shapes: [
|
|
165
|
+
{ shape: 'cylinder', size: [0.5, 0.8, 0.5], offset: [0, 0.4, 0], color: 'team' },
|
|
166
|
+
{ shape: 'sphere', size: [0.38, 0.38, 0.38], offset: [0, 0.98, 0], color: [0.87, 0.68, 0.53] },
|
|
167
|
+
{ shape: 'cone', size: [0.5, 0.22, 0.5], offset: [0, 1.22, 0], color: STRAW },
|
|
168
|
+
// what it carries home: a lump of gold, a log
|
|
169
|
+
{ shape: 'sphere', size: [0.42, 0.36, 0.42], offset: [0, 0.75, -0.34], color: GOLD, carrying: 'gold' },
|
|
170
|
+
{ shape: 'cylinder', size: [0.22, 0.9, 0.22], offset: [0, 0.8, -0.34], color: WOOD, tilt: Math.PI / 2, turn: Math.PI / 2, carrying: 'lumber' }
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
[RTS.footman]: {
|
|
174
|
+
height: 1.8,
|
|
175
|
+
model: character('knight', 0.7, { hold: [hold('sword_1handed'), hold('shield_round', 'l')] }),
|
|
176
|
+
attacks: [ONE_HANDED],
|
|
177
|
+
shapes: person(1.8)
|
|
178
|
+
},
|
|
179
|
+
[RTS.rifleman]: {
|
|
180
|
+
height: 1.8,
|
|
181
|
+
model: character('ranger', 0.7, { hold: [hold('crossbow_2handed')], animations: { attack: 'Ranged_2H_Shoot' } }),
|
|
182
|
+
shot: { color: [0.4, 0.4, 0.42], size: 0.18 },
|
|
183
|
+
shapes: person(1.8)
|
|
184
|
+
},
|
|
185
|
+
[RTS.paladin]: {
|
|
186
|
+
height: 2.3,
|
|
187
|
+
model: character('knight', 0.92, { hold: [hold('sword_2handed')], animations: { attack: 'Melee_2H_Attack_Chop' } }),
|
|
188
|
+
attacks: [TWO_HANDED],
|
|
189
|
+
shapes: person(2.3)
|
|
190
|
+
},
|
|
191
|
+
[RTS.gnoll]: {
|
|
192
|
+
height: 1.6,
|
|
193
|
+
model: character('rogue_hooded', 0.62, { hold: [hold('axe_1handed')] }),
|
|
194
|
+
attacks: [ONE_HANDED],
|
|
195
|
+
shapes: person(1.6, [0.5, 0.38, 0.22])
|
|
196
|
+
},
|
|
197
|
+
[RTS.brute]: {
|
|
198
|
+
height: 2,
|
|
199
|
+
model: character('barbarian', 0.82, { hold: [hold('axe_2handed')] }),
|
|
200
|
+
attacks: [TWO_HANDED],
|
|
201
|
+
shapes: person(2, [0.45, 0.33, 0.2])
|
|
202
|
+
},
|
|
203
|
+
[RTS.ogre]: {
|
|
204
|
+
height: 3,
|
|
205
|
+
model: character('skeleton_warrior', 1.2, {
|
|
206
|
+
hold: [
|
|
207
|
+
{ src: 'skeleton-gear/axe.glb', at: 'handslot.r' },
|
|
208
|
+
{ src: 'skeleton-gear/shield_large_a.glb', at: 'handslot.l' }
|
|
209
|
+
]
|
|
210
|
+
}),
|
|
211
|
+
attacks: [ONE_HANDED],
|
|
212
|
+
shapes: person(3, [0.62, 0.52, 0.4])
|
|
213
|
+
},
|
|
214
|
+
[RTS.townhall]: structure('rts/castle_2.glb', 3.9, 4.6, [
|
|
215
|
+
{ shape: 'box', size: [3.6, 1.9, 3.6], offset: [0, 0.95, 0], color: STONE },
|
|
216
|
+
{ shape: 'pyramid', size: [4.4, 1.7, 4.4], offset: [0, 2.75, 0], color: 'team' }
|
|
217
|
+
]),
|
|
218
|
+
[RTS.keep]: structure('rts/castle_fortress.glb', 4.1, 6.6, [
|
|
219
|
+
{ shape: 'box', size: [3.7, 2.4, 3.7], offset: [0, 1.2, 0], color: STONE },
|
|
220
|
+
{ shape: 'pyramid', size: [4.4, 1.7, 4.4], offset: [0, 3.25, 0], color: 'team' }
|
|
221
|
+
]),
|
|
222
|
+
[RTS.farm]: structure('rts/small_farm.glb', 2.1, 2.3, [
|
|
223
|
+
{ shape: 'box', size: [1.7, 0.9, 1.7], offset: [0, 0.45, 0], color: WOOD },
|
|
224
|
+
{ shape: 'pyramid', size: [2.1, 0.95, 2.1], offset: [0, 1.37, 0], color: STRAW }
|
|
225
|
+
]),
|
|
226
|
+
[RTS.barracks]: structure('rts/barracks.glb', 3, 3.4, [
|
|
227
|
+
{ shape: 'box', size: [2.7, 1.6, 2.7], offset: [0, 0.8, 0], color: STONE },
|
|
228
|
+
{ shape: 'box', size: [2.9, 0.35, 2.9], offset: [0, 1.75, 0], color: 'team' }
|
|
229
|
+
]),
|
|
230
|
+
[RTS.blacksmith]: structure('village/blacksmith.glb', 2.9, 3.3, [
|
|
231
|
+
{ shape: 'box', size: [2.7, 1.4, 2.7], offset: [0, 0.7, 0], color: STONE },
|
|
232
|
+
{ shape: 'pyramid', size: [3.1, 1, 3.1], offset: [0, 1.9, 0], color: [0.2, 0.19, 0.2] }
|
|
233
|
+
]),
|
|
234
|
+
[RTS.tower]: {
|
|
235
|
+
...structure('rts/watch_tower.glb', 1.7, 4.4, [
|
|
236
|
+
{ shape: 'cylinder', size: [1.5, 2.8, 1.5], offset: [0, 1.4, 0], color: STONE },
|
|
237
|
+
{ shape: 'cone', size: [2, 1, 2], offset: [0, 3.8, 0], color: 'team' }
|
|
238
|
+
]),
|
|
239
|
+
// its arrows leave from its top
|
|
240
|
+
model: { ...building('rts/watch_tower.glb', 1.7), points: { launch: { offset: [0, 3.6, 0] }, roof: { offset: [0, 3.5, 0] } } },
|
|
241
|
+
shot: { color: [0.85, 0.75, 0.55], size: 0.2 }
|
|
242
|
+
},
|
|
243
|
+
[RTS.altar]: structure('rts/temple_3.glb', 2.9, 3.2, [
|
|
244
|
+
{ shape: 'box', size: [2.8, 0.5, 2.8], offset: [0, 0.25, 0], color: STONE },
|
|
245
|
+
{ shape: 'sphere', size: [0.8, 0.8, 0.8], offset: [0, 1.6, 0], color: 'team' }
|
|
246
|
+
]),
|
|
247
|
+
[RTS.shop]: {
|
|
248
|
+
height: 2.2,
|
|
249
|
+
model: building('rts/market_stalls.glb', 3.2),
|
|
250
|
+
shapes: [
|
|
251
|
+
{ shape: 'box', size: [3, 0.9, 2], offset: [0, 0.45, 0], color: WOOD },
|
|
252
|
+
{ shape: 'pyramid', size: [3.2, 0.8, 2.4], offset: [0, 1.4, 0], color: [0.75, 0.3, 0.25] }
|
|
253
|
+
]
|
|
254
|
+
},
|
|
255
|
+
[RTS.goldmine]: {
|
|
256
|
+
height: 3.2,
|
|
257
|
+
model: building('rts/mine.glb', 3.9),
|
|
258
|
+
shapes: [
|
|
259
|
+
{ shape: 'pyramid', size: [3.9, 2.6, 3.9], offset: [0, 1.3, 0], color: [0.36, 0.33, 0.3] },
|
|
260
|
+
{ shape: 'sphere', size: [0.6, 0.5, 0.6], offset: [1.4, 0.25, -1.9], color: GOLD }
|
|
261
|
+
],
|
|
262
|
+
// over a mine: nothing
|
|
263
|
+
unitHud: { plates: [] }
|
|
264
|
+
},
|
|
265
|
+
[RTS.tree]: {
|
|
266
|
+
height: 3,
|
|
267
|
+
looks: TREE_LOOKS,
|
|
268
|
+
mix: Object.keys(TREE_LOOKS),
|
|
269
|
+
vary: { scale: 0.25, lean: 0.07 },
|
|
270
|
+
shapes: [
|
|
271
|
+
{ shape: 'cylinder', size: [0.3, 1, 0.3], offset: [0, 0.5, 0], color: [0.36, 0.25, 0.15] },
|
|
272
|
+
{ shape: 'cone', size: [1.5, 1.5, 1.5], offset: [0, 1.5, 0], color: [0.14, 0.36, 0.17] }
|
|
273
|
+
],
|
|
274
|
+
unitHud: { plates: [] }
|
|
275
|
+
},
|
|
276
|
+
[CARRY.ship]: {
|
|
277
|
+
height: 4.4,
|
|
278
|
+
// a little more than the client's rock: it is a small ship
|
|
279
|
+
motion: { rock: 0.09 },
|
|
280
|
+
shapes: SHIP,
|
|
281
|
+
// it goes down where it is, with a splash and smoke, and takes what it carries with it
|
|
282
|
+
death: {
|
|
283
|
+
collapse: { look: 'sink', time: 3.5 },
|
|
284
|
+
fx: [
|
|
285
|
+
{ fx: 'splash', params: { size: 2.2 } },
|
|
286
|
+
{ fx: 'smoke', delay: 0.2, params: { size: 1.4, darkness: 0.5 } },
|
|
287
|
+
{ fx: 'ripple', delay: 0.6, params: { size: 3 } }
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
[CARRY.zeppelin]: {
|
|
292
|
+
height: 4.2,
|
|
293
|
+
altitude: 3.4,
|
|
294
|
+
// a slow, deep bob, as something that big hangs on the air
|
|
295
|
+
motion: { bob: 0.25 },
|
|
296
|
+
shapes: ZEPPELIN,
|
|
297
|
+
death: {
|
|
298
|
+
collapse: { look: 'sink', time: 2.5 },
|
|
299
|
+
fx: [
|
|
300
|
+
{ fx: 'explosion', params: { size: 1.4 } },
|
|
301
|
+
{ fx: 'smoke', delay: 0.3, params: { size: 1.6 } }
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
[CARRY.tower]: {
|
|
306
|
+
...structure('rts/watch_tower.glb', 1.9, 4.6, [
|
|
307
|
+
{ shape: 'cylinder', size: [1.6, 3, 1.6], offset: [0, 1.5, 0], color: STONE },
|
|
308
|
+
{ shape: 'cone', size: [2.1, 1.1, 2.1], offset: [0, 4, 0], color: 'team' }
|
|
309
|
+
]),
|
|
310
|
+
// the garrison's arrows leave from its top
|
|
311
|
+
model: { ...building('rts/watch_tower.glb', 1.9), points: { launch: { offset: [0, 3.8, 0] }, roof: { offset: [0, 3.7, 0] } } },
|
|
312
|
+
shot: { color: [0.85, 0.75, 0.55], size: 0.22 }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The hero's spells: the colour each is drawn in */
|
|
317
|
+
const spells: AbilityLooks = {
|
|
318
|
+
rtsHolyLight: { color: [1, 0.95, 0.45] },
|
|
319
|
+
rtsThunderClap: { color: [0.85, 0.75, 0.5] },
|
|
320
|
+
rtsDevotion: { color: [0.75, 0.85, 1] },
|
|
321
|
+
rtsStunned: { color: [1, 1, 0.3] },
|
|
322
|
+
rtsDrinkSalve: { color: [0.5, 1, 0.5] }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Looks down at a square of the field `size` cells across round a point: from far enough to see all of it */
|
|
326
|
+
const view = (client: GameClientApi, x: number, z: number, size: number) =>
|
|
327
|
+
client.camera.frame({ minX: x - size / 2, minZ: z - size / 2, maxX: x + size / 2, maxZ: z + size / 2 })
|
|
328
|
+
|
|
329
|
+
/** The player's state as the screen has it: resources, supply, upgrades */
|
|
330
|
+
const own = (client: GameClientApi) => client.session.snapshot().players.find((each) => each.id === PLAYER)?.own
|
|
331
|
+
/** The player's gold and lumber, and their supply, in a line */
|
|
332
|
+
const purseLine = (client: GameClientApi) => {
|
|
333
|
+
const state = own(client)
|
|
334
|
+
return state
|
|
335
|
+
? `${Math.floor(state.resources.gold ?? 0)} gold · ${Math.floor(state.resources.lumber ?? 0)} lumber · ${state.supplyUsed}/${state.supplyCap} supply`
|
|
336
|
+
: '—'
|
|
337
|
+
}
|
|
338
|
+
/** The live units of `owner`'s, of a type or all */
|
|
339
|
+
const unitsOf = (client: GameClientApi, owner: number, type?: string) =>
|
|
340
|
+
client.session.snapshot().entities.filter((each) => each.owner === owner && each.health > 0 && (type === undefined || each.type === type))
|
|
341
|
+
/** Selects the first of the player's of a type: its card and its queue show in the HUD */
|
|
342
|
+
const selectFirst = (client: GameClientApi, type: string) => {
|
|
343
|
+
const [first] = unitsOf(client, PLAYER, type)
|
|
344
|
+
if (first) client.controller.select([first.id])
|
|
345
|
+
}
|
|
346
|
+
/** A stat of the first of the player's of a type: what its type says, or what upgrades and items made it */
|
|
347
|
+
const statOf = (client: GameClientApi, type: string, stat: 'attackDamage' | 'armor', base: number) => {
|
|
348
|
+
const [first] = unitsOf(client, PLAYER, type)
|
|
349
|
+
return first ? Math.round(first.stats?.[stat] ?? base) : '—'
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The player's ship, zeppelin or tower as the screen has it */
|
|
353
|
+
const transportOf = (client: GameClientApi) =>
|
|
354
|
+
unitsOf(client, PLAYER).find((each) => each.type === CARRY.ship || each.type === CARRY.zeppelin || each.type === CARRY.tower)
|
|
355
|
+
/** How many are inside the player's transport, of how many it takes */
|
|
356
|
+
const aboard = (client: GameClientApi, capacity: number, gone = 'sunk, and all it carried') => {
|
|
357
|
+
const transport = transportOf(client)
|
|
358
|
+
if (!transport) return gone
|
|
359
|
+
return `${snapshotOf(transport, cargo)?.passengers.length ?? 0} of ${capacity}`
|
|
360
|
+
}
|
|
361
|
+
/** The player's footmen, archers and mages out in the world, on this side of `x` and on the other */
|
|
362
|
+
const sides = (client: GameClientApi, x: number) => {
|
|
363
|
+
const riders = unitsOf(client, PLAYER).filter((each) => each.type === UNIT.footman || each.type === UNIT.archer || each.type === UNIT.mage)
|
|
364
|
+
return `${riders.filter((each) => each.x < x).length} · ${riders.filter((each) => each.x >= x).length}`
|
|
365
|
+
}
|
|
366
|
+
/** Selects the player's transport: its card shows its Unload button, and a right-click sends it */
|
|
367
|
+
const selectTransport = (client: GameClientApi) => {
|
|
368
|
+
const transport = transportOf(client)
|
|
369
|
+
if (transport) client.controller.select([transport.id])
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export const category: CategoryLooks = {
|
|
373
|
+
view: (view) => ({
|
|
374
|
+
...view,
|
|
375
|
+
units: { ...view.units, looks: { ...view.units?.looks, ...looks } },
|
|
376
|
+
fx: {
|
|
377
|
+
...view.fx,
|
|
378
|
+
// the ready-made effects the buildings smoke, burn, go up and come down with: what another category gave first wins
|
|
379
|
+
defs: { ...presetFx(['smoke', 'fire', 'dust', 'spark-burst', 'splash', 'ripple', 'explosion']), ...view.fx?.defs },
|
|
380
|
+
abilities: { ...view.fx?.abilities, looks: { ...view.fx?.abilities?.looks, ...spells } }
|
|
381
|
+
},
|
|
382
|
+
hud: {
|
|
383
|
+
...view.hud,
|
|
384
|
+
resources: {
|
|
385
|
+
gold: { chip: 'background: radial-gradient(circle at 35% 30%, #fff3b8, #e8b52c 45%, #9c6f12)', color: '#ffd23c' },
|
|
386
|
+
lumber: {
|
|
387
|
+
chip: 'width: 14px; height: 10px; border-radius: 5px; background: linear-gradient(180deg, #c8935a, #7a4a22); box-shadow: inset -4px 0 0 #e2c08c',
|
|
388
|
+
color: '#1f4d25'
|
|
389
|
+
},
|
|
390
|
+
...view.hud?.resources
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}),
|
|
394
|
+
looks: {
|
|
395
|
+
'rts-harvest': {
|
|
396
|
+
ready: (client) => view(client, 22, 24, 19),
|
|
397
|
+
controls: [
|
|
398
|
+
button(
|
|
399
|
+
'Train a peasant',
|
|
400
|
+
(client) => client.session.send(train.make({ unitType: RTS.peasant })),
|
|
401
|
+
'Queues a peasant at the town hall for 75 gold and 1 supply: it walks out when its bar fills'
|
|
402
|
+
),
|
|
403
|
+
button(
|
|
404
|
+
'Idle ones to work',
|
|
405
|
+
(client) => client.session.send(backToWork.make({})),
|
|
406
|
+
'Every peasant standing about goes back to work: half to the gold mine, half to the nearest tree'
|
|
407
|
+
),
|
|
408
|
+
readout('Yours', purseLine),
|
|
409
|
+
readout('Peasants', (client) => `${unitsOf(client, PLAYER, RTS.peasant).length}`)
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
'rts-train': {
|
|
413
|
+
ready: (client) => {
|
|
414
|
+
view(client, 27, 21, 20)
|
|
415
|
+
// the barracks selected: its queue shows in the HUD, and its card trains
|
|
416
|
+
selectFirst(client, RTS.barracks)
|
|
417
|
+
},
|
|
418
|
+
controls: [
|
|
419
|
+
button(
|
|
420
|
+
'Queue 5 footmen',
|
|
421
|
+
(client) => {
|
|
422
|
+
for (let i = 0; i < 5; i++) client.session.send(train.make({ unitType: RTS.footman }))
|
|
423
|
+
},
|
|
424
|
+
'Queues five footmen at the barracks, each paid for as it is queued: watch its queue in the HUD and the gold go down'
|
|
425
|
+
),
|
|
426
|
+
button('Queue a rifleman', (client) => client.session.send(train.make({ unitType: RTS.rifleman })), 'Refused until a blacksmith stands'),
|
|
427
|
+
button(
|
|
428
|
+
'Put up a blacksmith',
|
|
429
|
+
(client) => client.session.send(smithy.make({})),
|
|
430
|
+
'A blacksmith goes up by the barracks: once it stands, riflemen can be trained'
|
|
431
|
+
),
|
|
432
|
+
button('Rally to the field', (client) => client.session.send(rally.make({})), 'What comes out walks to the flag'),
|
|
433
|
+
button(
|
|
434
|
+
'Lay it out again',
|
|
435
|
+
(client) => {
|
|
436
|
+
client.session.send(resetTrain.make({}))
|
|
437
|
+
setTimeout(() => selectFirst(client, RTS.barracks), 300)
|
|
438
|
+
},
|
|
439
|
+
'Puts the base back as it started, its queue emptied and the barracks selected'
|
|
440
|
+
),
|
|
441
|
+
readout('Yours', purseLine),
|
|
442
|
+
readout('Footmen · riflemen', (client) => `${unitsOf(client, PLAYER, RTS.footman).length} · ${unitsOf(client, PLAYER, RTS.rifleman).length}`)
|
|
443
|
+
]
|
|
444
|
+
},
|
|
445
|
+
'rts-build': {
|
|
446
|
+
ready: (client) => view(client, 25, 21, 20),
|
|
447
|
+
controls: [
|
|
448
|
+
button('Build a farm', (client) => client.session.send(build.make({ unitType: RTS.farm })), '80 gold, 20 lumber: 6 supply'),
|
|
449
|
+
button('Build barracks', (client) => client.session.send(build.make({ unitType: RTS.barracks })), '160 gold, 60 lumber'),
|
|
450
|
+
button('Build a tower', (client) => client.session.send(build.make({ unitType: RTS.tower })), 'Needs a finished barracks'),
|
|
451
|
+
button(
|
|
452
|
+
'Lay it out again',
|
|
453
|
+
(client) => client.session.send(resetBuild.make({})),
|
|
454
|
+
'Takes down what was built and puts the peasants and the purse back'
|
|
455
|
+
),
|
|
456
|
+
readout('Yours', purseLine)
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
'rts-tech': {
|
|
460
|
+
ready: (client) => view(client, 25, 22, 18),
|
|
461
|
+
controls: [
|
|
462
|
+
button('Research Forged Swords', (client) => client.session.send(research.make({ upgrade: 'rtsSwords' })), '+4 damage a level'),
|
|
463
|
+
button('Research Iron Plating', (client) => client.session.send(research.make({ upgrade: 'rtsPlating' })), '+10 armor a level'),
|
|
464
|
+
button('Upgrade to Keep', (client) => client.session.send(toKeep.make({})), 'The second and third levels of swords need it'),
|
|
465
|
+
readout(
|
|
466
|
+
'Footman damage · armor',
|
|
467
|
+
(client) => `${statOf(client, RTS.footman, 'attackDamage', 12)} · ${statOf(client, RTS.footman, 'armor', 10)}`
|
|
468
|
+
),
|
|
469
|
+
readout('Researched', (client) => {
|
|
470
|
+
const state = own(client)
|
|
471
|
+
const levels = Object.entries(state?.upgrades ?? {}).map(([id, level]) => `${id === 'rtsSwords' ? 'Swords' : 'Plating'} ${level}`)
|
|
472
|
+
const going = state?.researching.length ? ` (researching ${state.researching.length})` : ''
|
|
473
|
+
return (levels.join(', ') || 'nothing yet') + going
|
|
474
|
+
})
|
|
475
|
+
]
|
|
476
|
+
},
|
|
477
|
+
'rts-hero': {
|
|
478
|
+
ready: (client) => {
|
|
479
|
+
view(client, 25, 26, 22)
|
|
480
|
+
selectFirst(client, RTS.paladin)
|
|
481
|
+
},
|
|
482
|
+
controls: [
|
|
483
|
+
button(
|
|
484
|
+
'Hunt the gnolls',
|
|
485
|
+
(client) => client.session.send(hunt.make({ camp: 'gnolls' })),
|
|
486
|
+
'The paladin goes for the gnolls, the easiest camp: watch its experience grow'
|
|
487
|
+
),
|
|
488
|
+
button(
|
|
489
|
+
'Hunt the brutes',
|
|
490
|
+
(client) => client.session.send(hunt.make({ camp: 'brutes' })),
|
|
491
|
+
'The paladin goes for the brutes: harder, and worth more experience'
|
|
492
|
+
),
|
|
493
|
+
button(
|
|
494
|
+
'Take on the ogre',
|
|
495
|
+
(client) => client.session.send(hunt.make({ camp: 'ogre' })),
|
|
496
|
+
'The paladin goes for the ogre lord: too much for it at a low level, it may fall'
|
|
497
|
+
),
|
|
498
|
+
button('Give 300 experience', (client) => client.session.send(giveXp.make({})), 'Each level gives a point to learn with'),
|
|
499
|
+
button(
|
|
500
|
+
'Learn Holy Light',
|
|
501
|
+
(client) => client.session.send(learnSpell.make({ ability: 'rtsHolyLight' })),
|
|
502
|
+
"Spends a point on Holy Light, a heal: it shows on the paladin's card once learned"
|
|
503
|
+
),
|
|
504
|
+
button(
|
|
505
|
+
'Learn Thunder Clap',
|
|
506
|
+
(client) => client.session.send(learnSpell.make({ ability: 'rtsThunderClap' })),
|
|
507
|
+
'Spends a point on Thunder Clap, a blow to everything round the paladin that stuns'
|
|
508
|
+
),
|
|
509
|
+
button(
|
|
510
|
+
'Learn Devotion Aura',
|
|
511
|
+
(client) => client.session.send(learnSpell.make({ ability: 'rtsDevotionAura' })),
|
|
512
|
+
'Spends a point on Devotion Aura: armor for the paladin and its side round it'
|
|
513
|
+
),
|
|
514
|
+
button(
|
|
515
|
+
'Lay it out again',
|
|
516
|
+
(client) => {
|
|
517
|
+
client.session.send(resetHero.make({}))
|
|
518
|
+
setTimeout(() => selectFirst(client, RTS.paladin), 300)
|
|
519
|
+
},
|
|
520
|
+
'A new paladin at level one, the camps back, the paladin selected'
|
|
521
|
+
),
|
|
522
|
+
readout('Paladin', (client) => {
|
|
523
|
+
const hero = own(client)?.heroes?.find((each) => each.type === RTS.paladin)
|
|
524
|
+
return hero ? `level ${hero.level} · ${hero.points} to learn${hero.alive ? '' : ' · fallen'}` : '—'
|
|
525
|
+
})
|
|
526
|
+
]
|
|
527
|
+
},
|
|
528
|
+
'rts-creeps': {
|
|
529
|
+
ready: (client) => view(client, 27, 24, 26),
|
|
530
|
+
controls: [
|
|
531
|
+
button('Attack the west camp', (client) => client.session.send(attackCamp.make({ camp: 'west' })), 'Three gnolls'),
|
|
532
|
+
button('Attack the east camp', (client) => client.session.send(attackCamp.make({ camp: 'east' })), 'A brute and two gnolls'),
|
|
533
|
+
button('Attack the middle', (client) => client.session.send(attackCamp.make({ camp: 'middle' })), 'The ogre and two brutes'),
|
|
534
|
+
button('Pull back', (client) => client.session.send(pullBack.make({})), 'They chase as far as their leash, then go home and heal'),
|
|
535
|
+
button('Bring the camps back', (client) => client.session.send(resetCreeps.make({})), 'The three camps and the army back as they started'),
|
|
536
|
+
readout('Creeps · yours', (client) => `${unitsOf(client, 90).length} · ${unitsOf(client, PLAYER).length}`)
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
'rts-shop': {
|
|
540
|
+
ready: (client) => {
|
|
541
|
+
view(client, 26, 23, 14)
|
|
542
|
+
selectFirst(client, RTS.paladin)
|
|
543
|
+
},
|
|
544
|
+
controls: [
|
|
545
|
+
button('Buy Claws of Attack', (client) => client.session.send(buy.make({ item: 'rtsClaws' })), '300 gold: +12 damage'),
|
|
546
|
+
button('Buy Ring of Protection', (client) => client.session.send(buy.make({ item: 'rtsRingOfProtection' })), '250 gold: +15 armor'),
|
|
547
|
+
button('Buy Gloves of Haste', (client) => client.session.send(buy.make({ item: 'rtsGlovesOfHaste' })), '200 gold: attacks 20% faster'),
|
|
548
|
+
button('Buy a Healing Salve', (client) => client.session.send(buy.make({ item: 'rtsHealingSalve' })), '100 gold: two drinks of 400'),
|
|
549
|
+
button(
|
|
550
|
+
'Lay it out again',
|
|
551
|
+
(client) => {
|
|
552
|
+
client.session.send(resetShop.make({}))
|
|
553
|
+
setTimeout(() => selectFirst(client, RTS.paladin), 300)
|
|
554
|
+
},
|
|
555
|
+
'The paladin without its items, the gold back, the paladin selected'
|
|
556
|
+
),
|
|
557
|
+
readout('Gold', (client) => `${Math.floor(own(client)?.resources.gold ?? 0)}`),
|
|
558
|
+
readout(
|
|
559
|
+
'Paladin damage · armor',
|
|
560
|
+
(client) => `${statOf(client, RTS.paladin, 'attackDamage', 30)} · ${statOf(client, RTS.paladin, 'armor', 25)}`
|
|
561
|
+
)
|
|
562
|
+
]
|
|
563
|
+
},
|
|
564
|
+
'rts-ferry': {
|
|
565
|
+
ready: (client) => view(client, riverAt(20), 21, 30),
|
|
566
|
+
controls: [
|
|
567
|
+
button(
|
|
568
|
+
'Board them all',
|
|
569
|
+
(client) => client.session.send(carry.make({ step: 'board' })),
|
|
570
|
+
'Everyone on the bank walks down to the water by the ship and steps aboard, out of the world: watch the count aboard go up'
|
|
571
|
+
),
|
|
572
|
+
button(
|
|
573
|
+
'Sail across',
|
|
574
|
+
(client) => client.session.send(carry.make({ step: 'across' })),
|
|
575
|
+
'The ship sails over the river to the far jetty with whoever is aboard'
|
|
576
|
+
),
|
|
577
|
+
button(
|
|
578
|
+
'Unload on the far bank',
|
|
579
|
+
(client) => client.session.send(carry.make({ step: 'unload' })),
|
|
580
|
+
'The ship sails to the east bank if it is not there yet and lets everyone out on the land nearest it, in a ring'
|
|
581
|
+
),
|
|
582
|
+
button(
|
|
583
|
+
'Sail back',
|
|
584
|
+
(client) => client.session.send(carry.make({ step: 'back' })),
|
|
585
|
+
'The ship sails back to the west jetty, to take on whoever is left there'
|
|
586
|
+
),
|
|
587
|
+
button(
|
|
588
|
+
'Sink it',
|
|
589
|
+
(client) => client.session.send(carry.make({ step: 'sink' })),
|
|
590
|
+
'The ship goes down where it is, and everyone aboard goes down with it: sink it loaded mid-river to see'
|
|
591
|
+
),
|
|
592
|
+
button(
|
|
593
|
+
'Select the ship',
|
|
594
|
+
selectTransport,
|
|
595
|
+
'Selects the ship: its card has an Unload button (lit when land is near), and a right-click on the land sends it there'
|
|
596
|
+
),
|
|
597
|
+
button(
|
|
598
|
+
'Lay it out again',
|
|
599
|
+
(client) => client.session.send(resetFerry.make({})),
|
|
600
|
+
'The squad back on the west bank and a new ship at the jetty'
|
|
601
|
+
),
|
|
602
|
+
readout('Aboard', (client) => aboard(client, 8), 'How many the ship carries, of the eight it takes'),
|
|
603
|
+
readout('West bank · east bank', (client) => sides(client, riverAt(20)), 'The squad standing on each bank, not counting those aboard')
|
|
604
|
+
]
|
|
605
|
+
},
|
|
606
|
+
'rts-zeppelin': {
|
|
607
|
+
ready: (client) => view(client, edgeAt(20) + 1, 21, 32),
|
|
608
|
+
controls: [
|
|
609
|
+
button(
|
|
610
|
+
'Board them all',
|
|
611
|
+
(client) => client.session.send(carry.make({ step: 'board' })),
|
|
612
|
+
'Everyone below the wall walks under the zeppelin and climbs aboard, out of the world: watch the count aboard go up'
|
|
613
|
+
),
|
|
614
|
+
button(
|
|
615
|
+
'Fly over the wall',
|
|
616
|
+
(client) => client.session.send(carry.make({ step: 'across' })),
|
|
617
|
+
'The zeppelin flies up over the cliff and hangs over the plateau'
|
|
618
|
+
),
|
|
619
|
+
button(
|
|
620
|
+
'Unload on the plateau',
|
|
621
|
+
(client) => client.session.send(carry.make({ step: 'unload' })),
|
|
622
|
+
'The zeppelin flies over the plateau if it is not there yet and sets everyone down on top, where no ramp leads'
|
|
623
|
+
),
|
|
624
|
+
button(
|
|
625
|
+
'Fly back',
|
|
626
|
+
(client) => client.session.send(carry.make({ step: 'back' })),
|
|
627
|
+
'The zeppelin flies back down over the field below the wall'
|
|
628
|
+
),
|
|
629
|
+
button(
|
|
630
|
+
'Try to walk up',
|
|
631
|
+
(client) => {
|
|
632
|
+
const below = unitsOf(client, PLAYER).filter((each) => each.type !== CARRY.zeppelin && each.x < edgeAt(each.z))
|
|
633
|
+
client.session.send({
|
|
634
|
+
type: 'order',
|
|
635
|
+
entities: below.map((each) => each.id),
|
|
636
|
+
order: { type: 'move', point: { x: edgeAt(20) + 8, z: 20 } }
|
|
637
|
+
})
|
|
638
|
+
},
|
|
639
|
+
'Sends those below the wall to the plateau on foot: with no ramp up, they get as far as the foot of the cliff and stop'
|
|
640
|
+
),
|
|
641
|
+
button(
|
|
642
|
+
'Shoot it down',
|
|
643
|
+
(client) => client.session.send(carry.make({ step: 'sink' })),
|
|
644
|
+
'The zeppelin comes down in flames, and everyone aboard with it: shoot it down loaded over the wall to see'
|
|
645
|
+
),
|
|
646
|
+
button(
|
|
647
|
+
'Select the zeppelin',
|
|
648
|
+
selectTransport,
|
|
649
|
+
'Selects the zeppelin: its card has an Unload button, and a right-click on the plateau flies it there'
|
|
650
|
+
),
|
|
651
|
+
button(
|
|
652
|
+
'Lay it out again',
|
|
653
|
+
(client) => client.session.send(resetAirlift.make({})),
|
|
654
|
+
'The squad back below the wall and the zeppelin over them'
|
|
655
|
+
),
|
|
656
|
+
readout('Aboard', (client) => aboard(client, 8, 'shot down, and all it carried'), 'How many the zeppelin carries, of the eight it takes'),
|
|
657
|
+
readout(
|
|
658
|
+
'Below · on the plateau',
|
|
659
|
+
(client) => sides(client, edgeAt(20)),
|
|
660
|
+
'The squad standing below the wall and up on the plateau, not counting those aboard'
|
|
661
|
+
)
|
|
662
|
+
]
|
|
663
|
+
},
|
|
664
|
+
'rts-garrison': {
|
|
665
|
+
ready: (client) => view(client, TOWER.x, TOWER.z + 2, 28),
|
|
666
|
+
controls: [
|
|
667
|
+
button(
|
|
668
|
+
'Garrison the tower',
|
|
669
|
+
(client) => client.session.send(carry.make({ step: 'board' })),
|
|
670
|
+
'The four soldiers walk into the tower and are out of reach inside: it shoots now, harder for each one in it'
|
|
671
|
+
),
|
|
672
|
+
button(
|
|
673
|
+
'Send in the gnolls',
|
|
674
|
+
(client) => client.session.send(gnollsCome.make({})),
|
|
675
|
+
'Six gnolls come down the road for the tower: an empty one does not shoot'
|
|
676
|
+
),
|
|
677
|
+
button(
|
|
678
|
+
'Let them out',
|
|
679
|
+
(client) => client.session.send(carry.make({ step: 'unload' })),
|
|
680
|
+
'Everyone inside comes out round the foot of the tower, and it stops shooting'
|
|
681
|
+
),
|
|
682
|
+
button(
|
|
683
|
+
'Bring the tower down',
|
|
684
|
+
(client) => client.session.send(carry.make({ step: 'sink' })),
|
|
685
|
+
'The tower falls: a building lets those inside out alive, where a ship takes them down with it'
|
|
686
|
+
),
|
|
687
|
+
button('Select the tower', selectTransport, 'Selects the tower: its card has an Unload button that lets them out'),
|
|
688
|
+
button(
|
|
689
|
+
'Lay it out again',
|
|
690
|
+
(client) => client.session.send(resetGarrison.make({})),
|
|
691
|
+
'The tower, the hall and the four soldiers back, the gnolls gone'
|
|
692
|
+
),
|
|
693
|
+
readout('Inside', (client) => aboard(client, 4, 'fallen: they came out'), 'How many are in the tower, of the four it takes'),
|
|
694
|
+
readout('Tower damage', (client) => `${statOf(client, CARRY.tower, 'attackDamage', 4)}`, 'Its arrows: 4, and 12 more for each one inside'),
|
|
695
|
+
readout('Gnolls', (client) => `${unitsOf(client, ENEMY).length}`, 'Gnolls still standing')
|
|
696
|
+
]
|
|
697
|
+
},
|
|
698
|
+
'rts-ai': {
|
|
699
|
+
ready: (client) => view(client, HOME.x, HOME.z + 4, 22),
|
|
700
|
+
controls: [
|
|
701
|
+
button('Look at your base', (client) => client.camera.panTo(HOME, 0.8), 'The camera pans to your base in the south west'),
|
|
702
|
+
button(
|
|
703
|
+
'Look at theirs',
|
|
704
|
+
(client) => client.camera.panTo(THEIRS, 0.8),
|
|
705
|
+
"The camera pans to the computer's base in the north east: watch it work and build"
|
|
706
|
+
),
|
|
707
|
+
button('Tell it to attack now', (client) => client.session.send(attackNow.make({})), 'What a mission script tells its computer'),
|
|
708
|
+
button('Start again', (client) => client.session.send(resetAi.make({})), 'Both bases back as they started, the computer seated again'),
|
|
709
|
+
readout('Yours', purseLine),
|
|
710
|
+
readout('Army: yours · theirs', (client) => {
|
|
711
|
+
const army = (owner: number) => unitsOf(client, owner).filter((each) => each.type === RTS.footman || each.type === RTS.rifleman).length
|
|
712
|
+
return `${army(PLAYER)} · ${army(ENEMY)}`
|
|
713
|
+
}),
|
|
714
|
+
readout(
|
|
715
|
+
'Their buildings',
|
|
716
|
+
(client) => `${unitsOf(client, ENEMY).filter((each) => ![RTS.peasant, RTS.footman, RTS.rifleman].includes(each.type as never)).length}`
|
|
717
|
+
)
|
|
718
|
+
]
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|