@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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys of the game's own: actions with a name, a label, a group and default keys (`ActionDef`), added to the client's
|
|
3
|
+
* while the game runs (`client.keys.define`; a game says them once in its view: `input.keys.actions.add`). The player
|
|
4
|
+
* can rebind them on the Keys screen of the pause menu as any of the client's; here a button rebinds one in code
|
|
5
|
+
* (`client.keys.bind`). A legend of the HUD's (`hud.widgets`) lists them with the keys they are on now, and follows a
|
|
6
|
+
* rebinding (`client.keys.onChange`)
|
|
7
|
+
*/
|
|
8
|
+
import type { GameClientApi, HudWidgetDef } from '@v12-engine/client'
|
|
9
|
+
import { PLAYER } from '../common/units'
|
|
10
|
+
import { gather } from './index'
|
|
11
|
+
|
|
12
|
+
/** The example's actions, by id: what each does, and the keys it comes with */
|
|
13
|
+
export const ACTIONS = [
|
|
14
|
+
{
|
|
15
|
+
id: 'gui.gather',
|
|
16
|
+
label: 'Gather the army where the camera looks',
|
|
17
|
+
default: ['KeyR'],
|
|
18
|
+
run: (client: GameClientApi) => {
|
|
19
|
+
const { x, z } = client.camera.pose()
|
|
20
|
+
client.session.send(gather.make({ at: { x, z } }))
|
|
21
|
+
client.ping(x, z, [0.4, 0.8, 1])
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'gui.cheer',
|
|
26
|
+
label: 'Everyone jumps for joy',
|
|
27
|
+
default: ['KeyC'],
|
|
28
|
+
run: (client: GameClientApi) => {
|
|
29
|
+
for (const unit of client.session.snapshot().entities) if (unit.owner === PLAYER && unit.health > 0) client.playClip(unit.id, 'Jump_Full_Short')
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'gui.hero',
|
|
34
|
+
label: 'Select the knight and look at him',
|
|
35
|
+
default: ['KeyZ'],
|
|
36
|
+
run: (client: GameClientApi) => {
|
|
37
|
+
const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
|
|
38
|
+
if (!hero) return
|
|
39
|
+
client.controller.select([hero.id])
|
|
40
|
+
client.camera.lookAt({ x: hero.x, z: hero.z }, { seconds: 0.5 })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
] as const
|
|
44
|
+
|
|
45
|
+
/** Adds the actions to the client's keys; gives back what takes them away */
|
|
46
|
+
export function defineKeys(client: GameClientApi): () => void {
|
|
47
|
+
for (const action of ACTIONS)
|
|
48
|
+
client.keys.define({ id: action.id, label: action.label, group: 'Examples', default: action.default, run: action.run })
|
|
49
|
+
return () => {
|
|
50
|
+
for (const action of ACTIONS) client.keys.forget(action.id)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The legend: each action with its keys as the player's keyboard shows them, drawn again when a binding changes */
|
|
55
|
+
export function keysLegend(client: GameClientApi): HudWidgetDef {
|
|
56
|
+
return {
|
|
57
|
+
id: 'gui.keys',
|
|
58
|
+
place: 'left',
|
|
59
|
+
mount(element) {
|
|
60
|
+
const panel = document.createElement('div')
|
|
61
|
+
panel.style.cssText =
|
|
62
|
+
'pointer-events:auto;width:270px;padding:12px 14px;border-radius:8px;background:rgba(12,14,20,.88);border:1px solid #3c4252;color:#e8e2d0;font:13px/1.35 system-ui,sans-serif'
|
|
63
|
+
element.append(panel)
|
|
64
|
+
const draw = () => {
|
|
65
|
+
panel.innerHTML = '<div style="font:600 15px system-ui;color:#f2d58a;margin-bottom:8px">Your keys</div>'
|
|
66
|
+
for (const action of ACTIONS) {
|
|
67
|
+
const row = document.createElement('div')
|
|
68
|
+
row.style.cssText = 'display:flex;gap:10px;align-items:center;margin:6px 0'
|
|
69
|
+
const key = document.createElement('kbd')
|
|
70
|
+
key.textContent = client.keys.keys(action.id).join(' / ') || '—'
|
|
71
|
+
key.style.cssText =
|
|
72
|
+
'min-width:28px;text-align:center;padding:4px 7px;border-radius:5px;background:linear-gradient(#3a3f4c,#1d2028);border:1px solid #5a6070;box-shadow:0 2px 0 #0a0b0e;font:700 13px system-ui;color:#ffe9a8'
|
|
73
|
+
const words = document.createElement('span')
|
|
74
|
+
words.textContent = action.label
|
|
75
|
+
row.append(key, words)
|
|
76
|
+
panel.append(row)
|
|
77
|
+
}
|
|
78
|
+
const note = document.createElement('div')
|
|
79
|
+
note.style.cssText = 'margin-top:8px;color:#8a8f9c;font-size:11px'
|
|
80
|
+
note.textContent = 'Rebind them on the Keys screen of the menu (F10), or with the buttons on the card'
|
|
81
|
+
panel.append(note)
|
|
82
|
+
}
|
|
83
|
+
draw()
|
|
84
|
+
const stop = client.keys.onChange(draw)
|
|
85
|
+
return () => {
|
|
86
|
+
stop()
|
|
87
|
+
panel.remove()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HUD's own parts, changed while the game runs: the console laid out centred or in the corners
|
|
3
|
+
* (`client.hud.setConsole`), a theme by name (`client.theme.set`: the colours, the frames' metal, the typefaces),
|
|
4
|
+
* parts hidden and shown (`client.hud.hide`), the HUD's size (`client.hud.setScale`), the top bar's title
|
|
5
|
+
* (`client.hud.setTitle`), the minimap's size (`client.hud.setMinimap`), or the whole HUD hidden (`client.hideHud`).
|
|
6
|
+
* Each is put back as the example is left
|
|
7
|
+
*/
|
|
8
|
+
import type { GameClientApi } from '@v12-engine/client'
|
|
9
|
+
|
|
10
|
+
/** The themes to pick from: the client's own (`themeNames()` lists every one, the editor's too) */
|
|
11
|
+
export const THEMES = ['classic', 'stone', 'moonlight', 'plain', 'iron', 'bone', 'jade'] as const
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The parts the toggles hide, by the client's ids (`HUD_WIDGETS`), with their words: those this game shows. The top
|
|
15
|
+
* bar's clock and resources, and the messages, are hidden the same way (`'clock'`, `'resources'`, `'messages'`): this
|
|
16
|
+
* game keeps no resources nor time, and says nothing to show
|
|
17
|
+
*/
|
|
18
|
+
export const PARTS = [
|
|
19
|
+
['minimap', 'Minimap'],
|
|
20
|
+
['card', 'Command card'],
|
|
21
|
+
['selection', 'Selection']
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
/** What the example changed, to put back as it is left: the whole HUD hidden */
|
|
25
|
+
const changed: { shown?: () => void } = {}
|
|
26
|
+
|
|
27
|
+
/** Hides the whole HUD, or shows it again (`client.hideHud` gives back what shows it) */
|
|
28
|
+
export function hideAll(client: GameClientApi, hidden: boolean): void {
|
|
29
|
+
changed.shown?.()
|
|
30
|
+
changed.shown = hidden ? client.hideHud() : undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Puts back everything the example changed: the HUD as the game's view says it */
|
|
34
|
+
export function restoreHud(client: GameClientApi): void {
|
|
35
|
+
hideAll(client, false)
|
|
36
|
+
client.hud.setMinimap({ size: 212 })
|
|
37
|
+
client.hud.setConsole('centered')
|
|
38
|
+
client.theme.reset()
|
|
39
|
+
for (const [id] of PARTS) client.hud.hide(id, false)
|
|
40
|
+
client.hud.setScale(1)
|
|
41
|
+
client.hud.setTitle(undefined)
|
|
42
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interface examples on the client's side: their controls, and what each puts up as it is opened (`ready`) and
|
|
3
|
+
* takes down as it is left. Each interface is a file of its own beside this one: Babylon's GUI on the screen
|
|
4
|
+
* (`babylonScreen.ts`) and in the world (`babylonWorld.ts`), a HUD part in Solid (`solidPart.ts`) and one in plain HTML
|
|
5
|
+
* (`htmlPart.ts`), the HUD's own parts rearranged (`layout.ts`), plates over units (`plates.ts`), a minimap
|
|
6
|
+
* (`minimap.ts`), a radial menu (`radial.ts`), a whole interface of the game's own (`ownInterface.ts`) and keys
|
|
7
|
+
* (`keys.ts`)
|
|
8
|
+
*/
|
|
9
|
+
import type { GameClientApi } from '@v12-engine/client'
|
|
10
|
+
import { button, type CategoryLooks, select, slider, toggle } from '../../shell/look'
|
|
11
|
+
import { aim } from '../cinematics/looks'
|
|
12
|
+
import { babylonPanel } from './babylonScreen'
|
|
13
|
+
import { babylonWorld, POST } from './babylonWorld'
|
|
14
|
+
import { rosterPart } from './htmlPart'
|
|
15
|
+
import { charge, gather, resetCamp } from './index'
|
|
16
|
+
import { ACTIONS, defineKeys, keysLegend } from './keys'
|
|
17
|
+
import { hideAll, PARTS, restoreHud, THEMES } from './layout'
|
|
18
|
+
import { minimapLayer, ownMinimap } from './minimap'
|
|
19
|
+
import { ownInterface } from './ownInterface'
|
|
20
|
+
import { DESIGNS, restorePlates, setDesign } from './plates'
|
|
21
|
+
import { closeRadial, radialRightClick } from './radial'
|
|
22
|
+
import { armyPart } from './solidPart'
|
|
23
|
+
|
|
24
|
+
/** The camp from the south, both sides in the picture */
|
|
25
|
+
const overTheCamp = (client: GameClientApi, distance = 30) => aim(client, { x: 24, z: 23 }, distance)
|
|
26
|
+
|
|
27
|
+
/** The two buttons every camp has: the battle, and the camp laid out again */
|
|
28
|
+
const battle = [
|
|
29
|
+
button(
|
|
30
|
+
'Charge',
|
|
31
|
+
(client) => client.session.send(charge.make({})),
|
|
32
|
+
'Sends both camps at each other: watch the interface follow the fight, health falling and units dying.'
|
|
33
|
+
),
|
|
34
|
+
button(
|
|
35
|
+
'Lay it out again',
|
|
36
|
+
(client) => client.session.send(resetCamp.make({})),
|
|
37
|
+
'Puts both camps back as they started, everyone whole, for another charge.'
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/** What the minimap example has up now: the game's own minimap, or a layer on the client's */
|
|
42
|
+
const minimapNow: { off?: () => void } = {}
|
|
43
|
+
|
|
44
|
+
/** The interface of the game's own while it is up: what takes it down */
|
|
45
|
+
const own: { off?: () => void } = {}
|
|
46
|
+
|
|
47
|
+
export const category: CategoryLooks = {
|
|
48
|
+
looks: {
|
|
49
|
+
'gui-babylon-screen': {
|
|
50
|
+
controls: battle,
|
|
51
|
+
ready: (client) => {
|
|
52
|
+
overTheCamp(client)
|
|
53
|
+
return babylonPanel(client)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
'gui-babylon-world': {
|
|
57
|
+
controls: [
|
|
58
|
+
...battle,
|
|
59
|
+
button(
|
|
60
|
+
'Gather at the sign',
|
|
61
|
+
(client) => client.session.send(gather.make({ at: { x: POST.x - 2, z: POST.z - 2 } })),
|
|
62
|
+
'Sends your army to the signpost: the same as clicking the button on the sign itself, which is Babylon GUI drawn in the world.'
|
|
63
|
+
)
|
|
64
|
+
],
|
|
65
|
+
ready: (client) => {
|
|
66
|
+
overTheCamp(client, 28)
|
|
67
|
+
return babylonWorld(client)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
'gui-solid': {
|
|
71
|
+
controls: battle,
|
|
72
|
+
ready: (client) => {
|
|
73
|
+
overTheCamp(client)
|
|
74
|
+
return client.hud.widgets.add(armyPart(client))
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
'gui-html': {
|
|
78
|
+
controls: battle,
|
|
79
|
+
ready: (client) => {
|
|
80
|
+
overTheCamp(client)
|
|
81
|
+
return client.hud.widgets.add(rosterPart(client))
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
'gui-hud-layout': {
|
|
85
|
+
controls: [
|
|
86
|
+
select(
|
|
87
|
+
'Console',
|
|
88
|
+
['centered', 'corners'],
|
|
89
|
+
'centered',
|
|
90
|
+
(client, layout) => client.hud.setConsole(layout as 'centered' | 'corners'),
|
|
91
|
+
'How the bottom console is laid out: one centred bar, or the minimap and the card apart in the corners.'
|
|
92
|
+
),
|
|
93
|
+
select(
|
|
94
|
+
'Theme',
|
|
95
|
+
THEMES,
|
|
96
|
+
'classic',
|
|
97
|
+
(client, theme) => client.theme.set(theme),
|
|
98
|
+
'The HUD’s theme: its colours, the metal of its frames and its typefaces change at once.'
|
|
99
|
+
),
|
|
100
|
+
slider(
|
|
101
|
+
'Size',
|
|
102
|
+
{ min: 0.7, max: 1.3, step: 0.05, value: 1 },
|
|
103
|
+
(client, size) => client.hud.setScale(size),
|
|
104
|
+
'How big the whole HUD is drawn, from 70% to 130%.'
|
|
105
|
+
),
|
|
106
|
+
slider(
|
|
107
|
+
'Minimap size',
|
|
108
|
+
{ min: 150, max: 320, step: 10, value: 212 },
|
|
109
|
+
(client, size) => client.hud.setMinimap({ size }),
|
|
110
|
+
'How big the minimap is, in pixels: the console makes room for it.'
|
|
111
|
+
),
|
|
112
|
+
...PARTS.map(([id, words]) =>
|
|
113
|
+
toggle(
|
|
114
|
+
words,
|
|
115
|
+
true,
|
|
116
|
+
(client, on) => client.hud.hide(id, !on),
|
|
117
|
+
`Shows or hides this one part of the HUD, the ${words.toLowerCase()}: the rest stays where it is.`
|
|
118
|
+
)
|
|
119
|
+
),
|
|
120
|
+
button('Title: “The Camp”', (client) => client.hud.setTitle('The Camp'), 'Sets the title the top bar shows to “The Camp”.'),
|
|
121
|
+
toggle(
|
|
122
|
+
'Hide the whole HUD',
|
|
123
|
+
false,
|
|
124
|
+
(client, on) => hideAll(client, on),
|
|
125
|
+
'Hides the whole HUD at once, for a clean picture (client.hideHud); off, it comes back as it was.'
|
|
126
|
+
),
|
|
127
|
+
button('Put it all back', (client) => restoreHud(client), 'Undoes every change above: the HUD as the game’s view says it.')
|
|
128
|
+
],
|
|
129
|
+
ready: (client) => {
|
|
130
|
+
overTheCamp(client)
|
|
131
|
+
// the knight selected: the selection's panel has him to show
|
|
132
|
+
const hero = client.session.snapshot().entities.find((each) => each.name === 'hero')
|
|
133
|
+
if (hero) client.controller.select([hero.id])
|
|
134
|
+
return () => restoreHud(client)
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
'gui-plates': {
|
|
138
|
+
controls: [
|
|
139
|
+
select(
|
|
140
|
+
'Design',
|
|
141
|
+
Object.keys(DESIGNS),
|
|
142
|
+
'Hero card',
|
|
143
|
+
(client, design) => setDesign(client, design),
|
|
144
|
+
'What the plate over each unit shows: a thin bar only while hurt, a name over a bar, a hero’s card… Charge to see the bars fall.'
|
|
145
|
+
),
|
|
146
|
+
select(
|
|
147
|
+
'Drawn as',
|
|
148
|
+
['canvas', 'dom', 'gui'],
|
|
149
|
+
'canvas',
|
|
150
|
+
(client, render) => (client.unitHud.look.render = render as 'canvas' | 'dom' | 'gui'),
|
|
151
|
+
'What draws the plates: one canvas over the picture, an HTML element each, or Babylon GUI. They look the same; what it costs differs.'
|
|
152
|
+
),
|
|
153
|
+
toggle(
|
|
154
|
+
'Bars over everyone',
|
|
155
|
+
false,
|
|
156
|
+
(client, on) => (client.showAllBars = on),
|
|
157
|
+
'Shows the plates over every unit, not only the selected, hovered or hurt, as holding Alt does: plain to see with the Bars when hurt design, which shows none till then.'
|
|
158
|
+
),
|
|
159
|
+
...battle
|
|
160
|
+
],
|
|
161
|
+
ready: (client) => {
|
|
162
|
+
overTheCamp(client, 24)
|
|
163
|
+
setDesign(client, 'Hero card')
|
|
164
|
+
return () => restorePlates(client)
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
'gui-minimap': {
|
|
168
|
+
controls: [
|
|
169
|
+
select(
|
|
170
|
+
'Minimap',
|
|
171
|
+
['ours', 'the client’s with a layer', 'the client’s'],
|
|
172
|
+
'ours',
|
|
173
|
+
(client, which) => {
|
|
174
|
+
minimapNow.off?.()
|
|
175
|
+
minimapNow.off = which === 'ours' ? ownMinimap(client) : which === 'the client’s' ? undefined : minimapLayer(client)
|
|
176
|
+
},
|
|
177
|
+
'Which minimap shows, bottom left: one the game draws itself, the client’s with the camps and a ring round the knight drawn on it, or the client’s as it is.'
|
|
178
|
+
),
|
|
179
|
+
...battle
|
|
180
|
+
],
|
|
181
|
+
ready: (client) => {
|
|
182
|
+
overTheCamp(client)
|
|
183
|
+
minimapNow.off = ownMinimap(client)
|
|
184
|
+
return () => {
|
|
185
|
+
minimapNow.off?.()
|
|
186
|
+
minimapNow.off = undefined
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
'gui-radial': {
|
|
191
|
+
view: (view) => ({ ...view, input: { ...view.input, onRightClick: radialRightClick } }),
|
|
192
|
+
controls: [
|
|
193
|
+
button(
|
|
194
|
+
'Select your army',
|
|
195
|
+
(client) =>
|
|
196
|
+
client.controller.select(
|
|
197
|
+
client.session
|
|
198
|
+
.snapshot()
|
|
199
|
+
.entities.filter((each) => each.owner === client.session.playerId)
|
|
200
|
+
.map((each) => each.id)
|
|
201
|
+
),
|
|
202
|
+
'Selects all your units: then right-click the ground to open the radial menu of orders round the pointer.'
|
|
203
|
+
),
|
|
204
|
+
...battle
|
|
205
|
+
],
|
|
206
|
+
ready: (client) => {
|
|
207
|
+
overTheCamp(client)
|
|
208
|
+
client.controller.select(
|
|
209
|
+
client.session
|
|
210
|
+
.snapshot()
|
|
211
|
+
.entities.filter((each) => each.owner === client.session.playerId)
|
|
212
|
+
.map((each) => each.id)
|
|
213
|
+
)
|
|
214
|
+
return () => closeRadial()
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
'gui-own-interface': {
|
|
218
|
+
controls: [
|
|
219
|
+
toggle(
|
|
220
|
+
'Ours in place of the HUD',
|
|
221
|
+
true,
|
|
222
|
+
(client, on) => {
|
|
223
|
+
own.off?.()
|
|
224
|
+
own.off = on ? ownInterface(client) : undefined
|
|
225
|
+
},
|
|
226
|
+
'Whether the game’s own interface replaces the client’s HUD: off, the client’s HUD is back.'
|
|
227
|
+
),
|
|
228
|
+
...battle
|
|
229
|
+
],
|
|
230
|
+
ready: (client) => {
|
|
231
|
+
overTheCamp(client)
|
|
232
|
+
own.off = ownInterface(client)
|
|
233
|
+
return () => {
|
|
234
|
+
own.off?.()
|
|
235
|
+
own.off = undefined
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
'gui-keys': {
|
|
240
|
+
controls: [
|
|
241
|
+
...ACTIONS.map((action) =>
|
|
242
|
+
button(
|
|
243
|
+
action.label,
|
|
244
|
+
(client) => client.keys.press(action.id),
|
|
245
|
+
`Runs this action as pressing its key does (${action.default[0].slice(3)} at first): the legend lists each with its key.`
|
|
246
|
+
)
|
|
247
|
+
),
|
|
248
|
+
button(
|
|
249
|
+
'Put Gather on G',
|
|
250
|
+
(client) => client.keys.bind('gui.gather', ['KeyG']),
|
|
251
|
+
'Moves the Gather action from R to G, in code (client.keys.bind): the legend changes, and G now gathers.'
|
|
252
|
+
),
|
|
253
|
+
button(
|
|
254
|
+
'Keys back',
|
|
255
|
+
(client) => ACTIONS.forEach((action) => client.keys.reset(action.id)),
|
|
256
|
+
'Puts every action back on its first key: the legend follows.'
|
|
257
|
+
)
|
|
258
|
+
],
|
|
259
|
+
ready: (client) => {
|
|
260
|
+
overTheCamp(client)
|
|
261
|
+
const forget = defineKeys(client)
|
|
262
|
+
const legend = client.hud.widgets.add(keysLegend(client))
|
|
263
|
+
return () => {
|
|
264
|
+
legend()
|
|
265
|
+
forget()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimap of the game's own, two ways:
|
|
3
|
+
*
|
|
4
|
+
* - In the client's place: the client's hidden (`client.hud.hide('minimap')`) and a widget of the game's own with a
|
|
5
|
+
* `mount`, drawing a canvas of its own every frame (the field, the units as dots in their side's colour, the frame
|
|
6
|
+
* of what the camera sees) and turning a click into a look at that spot
|
|
7
|
+
* - Over the client's: a layer drawn on the client's own minimap (`client.hud.addMinimapLayer`), here the two camps'
|
|
8
|
+
* grounds and a ring pulsing round the knight
|
|
9
|
+
*/
|
|
10
|
+
import type { GameClientApi } from '@v12-engine/client'
|
|
11
|
+
import { PLAYER } from '../common/units'
|
|
12
|
+
|
|
13
|
+
/** The field's size in cells: the examples' field (`exampleField`) */
|
|
14
|
+
const SIZE = 48
|
|
15
|
+
/** The minimap's size in pixels */
|
|
16
|
+
const PIXELS = 220
|
|
17
|
+
|
|
18
|
+
/** The minimap of the game's own, in the place of the client's; gives back what takes it away */
|
|
19
|
+
export function ownMinimap(client: GameClientApi): () => void {
|
|
20
|
+
// the client's minimap and its buttons hidden, and the game's own put over the map in the bottom left
|
|
21
|
+
client.hud.hide('minimap')
|
|
22
|
+
const remove = client.hud.widgets.add({
|
|
23
|
+
id: 'gui.minimap',
|
|
24
|
+
place: 'bottom-left',
|
|
25
|
+
mount(element) {
|
|
26
|
+
const canvas = document.createElement('canvas')
|
|
27
|
+
canvas.width = canvas.height = PIXELS * devicePixelRatio
|
|
28
|
+
canvas.style.cssText = `width:${PIXELS}px;height:${PIXELS}px;border-radius:50%;cursor:crosshair;pointer-events:auto;display:block;
|
|
29
|
+
box-shadow:0 0 0 3px #0e1118,0 0 0 5px #c9a35a,0 8px 20px rgba(0,0,0,.6)`
|
|
30
|
+
element.append(canvas)
|
|
31
|
+
const context = canvas.getContext('2d')!
|
|
32
|
+
const scale = canvas.width / SIZE
|
|
33
|
+
// north up: the map's z grows up the minimap
|
|
34
|
+
const toMap = (x: number, z: number) => ({ x: x * scale, y: canvas.height - z * scale })
|
|
35
|
+
|
|
36
|
+
let frame = 0
|
|
37
|
+
const draw = () => {
|
|
38
|
+
frame = requestAnimationFrame(draw)
|
|
39
|
+
const { width, height } = canvas
|
|
40
|
+
context.save()
|
|
41
|
+
context.clearRect(0, 0, width, height)
|
|
42
|
+
// round, like a radar
|
|
43
|
+
context.beginPath()
|
|
44
|
+
context.arc(width / 2, height / 2, width / 2, 0, Math.PI * 2)
|
|
45
|
+
context.clip()
|
|
46
|
+
const ground = context.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width / 2)
|
|
47
|
+
ground.addColorStop(0, '#3d6b2e')
|
|
48
|
+
ground.addColorStop(1, '#1d3418')
|
|
49
|
+
context.fillStyle = ground
|
|
50
|
+
context.fillRect(0, 0, width, height)
|
|
51
|
+
// a grid every 8 cells
|
|
52
|
+
context.strokeStyle = 'rgba(255,255,255,.07)'
|
|
53
|
+
context.lineWidth = 1
|
|
54
|
+
for (let cell = 8; cell < SIZE; cell += 8) {
|
|
55
|
+
context.beginPath()
|
|
56
|
+
context.moveTo(cell * scale, 0)
|
|
57
|
+
context.lineTo(cell * scale, height)
|
|
58
|
+
context.moveTo(0, cell * scale)
|
|
59
|
+
context.lineTo(width, cell * scale)
|
|
60
|
+
context.stroke()
|
|
61
|
+
}
|
|
62
|
+
// the units: a dot each, bigger for the knights
|
|
63
|
+
for (const unit of client.session.snapshot().entities) {
|
|
64
|
+
if (unit.health <= 0) continue
|
|
65
|
+
const at = toMap(unit.x, unit.z)
|
|
66
|
+
context.fillStyle = unit.owner === PLAYER ? '#5fb0ff' : '#ff5a44'
|
|
67
|
+
context.beginPath()
|
|
68
|
+
context.arc(at.x, at.y, (unit.type === 'knight' ? 4.5 : 2.6) * devicePixelRatio, 0, Math.PI * 2)
|
|
69
|
+
context.fill()
|
|
70
|
+
}
|
|
71
|
+
// what the camera sees, near enough: a box round where it looks, as wide as it is far away
|
|
72
|
+
const pose = client.camera.pose()
|
|
73
|
+
const half = pose.distance * 0.62
|
|
74
|
+
const corner = toMap(pose.x - half, pose.z + half * 0.6)
|
|
75
|
+
context.strokeStyle = '#fff'
|
|
76
|
+
context.lineWidth = 1.5 * devicePixelRatio
|
|
77
|
+
context.strokeRect(corner.x, corner.y, half * 2 * scale, half * 1.2 * scale)
|
|
78
|
+
context.restore()
|
|
79
|
+
}
|
|
80
|
+
draw()
|
|
81
|
+
|
|
82
|
+
// a click: the camera looks there
|
|
83
|
+
canvas.onpointerdown = (event) => {
|
|
84
|
+
const box = canvas.getBoundingClientRect()
|
|
85
|
+
const x = ((event.clientX - box.left) / box.width) * SIZE
|
|
86
|
+
const z = (1 - (event.clientY - box.top) / box.height) * SIZE
|
|
87
|
+
client.camera.lookAt({ x, z }, { seconds: 0.4 })
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
cancelAnimationFrame(frame)
|
|
91
|
+
canvas.remove()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
return () => {
|
|
96
|
+
remove()
|
|
97
|
+
client.hud.hide('minimap', false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A layer on the client's own minimap: the camps' grounds, and a ring round the knight; gives back what takes it away */
|
|
102
|
+
export function minimapLayer(client: GameClientApi): () => void {
|
|
103
|
+
return client.hud.addMinimapLayer({
|
|
104
|
+
id: 'gui.camps',
|
|
105
|
+
at: 'units',
|
|
106
|
+
draw: (context, frame) => {
|
|
107
|
+
const zone = (x: number, z: number, color: string) => {
|
|
108
|
+
const at = frame.toMap(x, z)
|
|
109
|
+
context.fillStyle = color
|
|
110
|
+
context.beginPath()
|
|
111
|
+
context.ellipse(at.x, at.y, 8 * frame.scale.x, 6 * frame.scale.y, 0, 0, Math.PI * 2)
|
|
112
|
+
context.fill()
|
|
113
|
+
}
|
|
114
|
+
zone(24, 16, 'rgba(80,160,255,.28)')
|
|
115
|
+
zone(24, 32, 'rgba(255,80,60,.28)')
|
|
116
|
+
const hero = frame.client.session.snapshot().entities.find((each) => each.name === 'hero')
|
|
117
|
+
if (!hero) return
|
|
118
|
+
const at = frame.toMap(hero.x, hero.z)
|
|
119
|
+
const pulse = (frame.now / 1000) % 1
|
|
120
|
+
context.strokeStyle = `rgba(255,220,90,${1 - pulse})`
|
|
121
|
+
context.lineWidth = 2
|
|
122
|
+
context.beginPath()
|
|
123
|
+
context.arc(at.x, at.y, (3 + pulse * 6) * frame.scale.x, 0, Math.PI * 2)
|
|
124
|
+
context.stroke()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An interface of the game's own in place of the whole HUD: the client's hidden (`client.hideHud()`, which gives back
|
|
3
|
+
* what shows it again), and a layer of plain HTML over the world in its place (`client.addLayer`). It has a bar across
|
|
4
|
+
* the top with the battle's numbers, a card for what is selected (from the HUD's state: `client.ui.selection()`), order
|
|
5
|
+
* buttons that send what the client's card would, and a log of the fallen. The selection, orders and camera are the
|
|
6
|
+
* client's as ever: only the drawing is the game's
|
|
7
|
+
*/
|
|
8
|
+
import type { GameClientApi } from '@v12-engine/client'
|
|
9
|
+
import { ENEMY, PLAYER } from '../common/units'
|
|
10
|
+
import { charge, gather, resetCamp } from './index'
|
|
11
|
+
|
|
12
|
+
const STYLE = `
|
|
13
|
+
.gui-own { position: absolute; inset: 0; pointer-events: none; font: 14px/1.3 'Trebuchet MS', system-ui, sans-serif; color: #eef3f7 }
|
|
14
|
+
.gui-own .glass { pointer-events: auto; background: linear-gradient(180deg, rgba(20, 34, 48, .92), rgba(8, 14, 22, .92)); border: 1px solid #4fd1c5;
|
|
15
|
+
box-shadow: 0 0 0 1px rgba(0,0,0,.6), 0 0 18px rgba(79, 209, 197, .25); border-radius: 4px }
|
|
16
|
+
.gui-own .top { position: absolute; top: 12px; left: 16px; display: flex; gap: 18px; align-items: center; padding: 8px 16px }
|
|
17
|
+
.gui-own .top b { color: #4fd1c5; letter-spacing: .18em; font-size: 13px }
|
|
18
|
+
.gui-own .top .num { font: 700 20px 'Trebuchet MS', sans-serif }
|
|
19
|
+
.gui-own .top .ours { color: #7cc4ff } .gui-own .top .theirs { color: #ff8b78 }
|
|
20
|
+
.gui-own .card { position: absolute; left: 16px; bottom: 16px; width: 300px; padding: 12px 14px }
|
|
21
|
+
.gui-own .card h4 { margin: 0 0 6px; font-size: 18px; color: #fff }
|
|
22
|
+
.gui-own .card .bar { height: 10px; border-radius: 5px; background: #0b1118; border: 1px solid #27404f; overflow: hidden; margin: 6px 0 }
|
|
23
|
+
.gui-own .card .bar i { display: block; height: 100%; background: linear-gradient(90deg, #22c55e, #4fd1c5) }
|
|
24
|
+
.gui-own .card small { color: #93a8b8 }
|
|
25
|
+
.gui-own .orders { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; gap: 8px; padding: 8px }
|
|
26
|
+
.gui-own .orders button { pointer-events: auto; cursor: pointer; width: 74px; height: 64px; border-radius: 4px; border: 1px solid #2c5a66;
|
|
27
|
+
background: #0e1a24; color: #dff; font: 600 12px 'Trebuchet MS', sans-serif; display: grid; place-items: center; gap: 2px }
|
|
28
|
+
.gui-own .orders button em { font-style: normal; font-size: 22px; color: #4fd1c5 }
|
|
29
|
+
.gui-own .orders button:hover { border-color: #4fd1c5; background: #12303a }
|
|
30
|
+
.gui-own .log { position: absolute; right: 16px; bottom: 16px; width: 260px; max-height: 180px; overflow: hidden; padding: 10px 12px;
|
|
31
|
+
display: flex; flex-direction: column-reverse; gap: 3px; font-size: 12px }
|
|
32
|
+
.gui-own .log div { color: #b9cad6 } .gui-own .log div.foe { color: #ffb4a6 }
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
/** The card's words for what is selected: the first one's name and health, and how many more */
|
|
36
|
+
function cardOf(client: GameClientApi): string {
|
|
37
|
+
const selection = client.ui.selection()
|
|
38
|
+
if (!selection.length) return '<h4>Nothing selected</h4><small>Click a unit, or drag a box round several</small>'
|
|
39
|
+
const first = selection[0]
|
|
40
|
+
const share = first.health / Math.max(1, first.maxHealth)
|
|
41
|
+
const stats = first.stats
|
|
42
|
+
.slice(0, 3)
|
|
43
|
+
.map((line) => `${line.label} ${line.value}`)
|
|
44
|
+
.join(' · ')
|
|
45
|
+
return `<h4>${first.name}${selection.length > 1 ? ` <small>and ${selection.length - 1} more</small>` : ''}</h4>
|
|
46
|
+
<div class="bar"><i style="width:${share * 100}%"></i></div>
|
|
47
|
+
<small>${Math.round(first.health)} / ${Math.round(first.maxHealth)} health${stats ? ` · ${stats}` : ''}</small>`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Hides the client's HUD and puts the game's own up; gives back what takes it down and shows the client's again */
|
|
51
|
+
export function ownInterface(client: GameClientApi): () => void {
|
|
52
|
+
const showHud = client.hideHud()
|
|
53
|
+
const root = document.createElement('div')
|
|
54
|
+
root.className = 'gui-own'
|
|
55
|
+
root.innerHTML = `<style>${STYLE}</style>
|
|
56
|
+
<div class="top glass"><b>ELMBROOK GARRISON</b>
|
|
57
|
+
<span>Yours <span class="num ours" data-ours>0</span></span>
|
|
58
|
+
<span>Theirs <span class="num theirs" data-theirs>0</span></span>
|
|
59
|
+
<span data-time>0:00</span></div>
|
|
60
|
+
<div class="card glass" data-card></div>
|
|
61
|
+
<div class="orders glass" data-orders></div>
|
|
62
|
+
<div class="log glass" data-log><div>The armies stand ready.</div></div>`
|
|
63
|
+
const removeLayer = client.addLayer(root, { under: 'hud' })
|
|
64
|
+
const $ = (name: string) => root.querySelector<HTMLElement>(`[data-${name}]`)!
|
|
65
|
+
|
|
66
|
+
/** The selected units of the player's own */
|
|
67
|
+
const mine = () =>
|
|
68
|
+
client.ui
|
|
69
|
+
.selection()
|
|
70
|
+
.filter((item) => item.own)
|
|
71
|
+
.map((item) => item.id)
|
|
72
|
+
const orders: [string, string, () => void][] = [
|
|
73
|
+
['⚔', 'Charge', () => client.session.send(charge.make({}))],
|
|
74
|
+
['■', 'Stop', () => client.session.send({ type: 'order', entities: mine(), order: { type: 'stop' } })],
|
|
75
|
+
['⛨', 'Hold', () => client.session.send({ type: 'order', entities: mine(), order: { type: 'hold' } })],
|
|
76
|
+
[
|
|
77
|
+
'✺',
|
|
78
|
+
'Gather',
|
|
79
|
+
() => {
|
|
80
|
+
const pose = client.camera.pose()
|
|
81
|
+
client.session.send(gather.make({ at: { x: pose.x, z: pose.z } }))
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
['↺', 'Again', () => client.session.send(resetCamp.make({}))]
|
|
85
|
+
]
|
|
86
|
+
for (const [glyph, label, run] of orders) {
|
|
87
|
+
const button = document.createElement('button')
|
|
88
|
+
button.type = 'button'
|
|
89
|
+
button.innerHTML = `<em>${glyph}</em>${label}`
|
|
90
|
+
button.onclick = run
|
|
91
|
+
$('orders').append(button)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// the selection's card, drawn again as the HUD's state changes
|
|
95
|
+
const stopCard = client.ui.watch(
|
|
96
|
+
() => cardOf(client),
|
|
97
|
+
(words) => ($('card').innerHTML = words),
|
|
98
|
+
{ immediate: true }
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
// the numbers, the clock and the log, from the snapshot
|
|
102
|
+
let known = new Map<number, { owner: number; name: string }>()
|
|
103
|
+
const tick = () => {
|
|
104
|
+
const { entities, tick } = client.session.snapshot()
|
|
105
|
+
const alive = entities.filter((each) => each.health > 0)
|
|
106
|
+
$('ours').textContent = String(alive.filter((each) => each.owner === PLAYER).length)
|
|
107
|
+
$('theirs').textContent = String(alive.filter((each) => each.owner === ENEMY).length)
|
|
108
|
+
const seconds = Math.floor(tick / client.session.tickRate)
|
|
109
|
+
$('time').textContent = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`
|
|
110
|
+
const now = new Map(alive.map((each) => [each.id, { owner: each.owner, name: client.session.registry.unit(each.type)?.name ?? each.type }]))
|
|
111
|
+
for (const [id, was] of known) {
|
|
112
|
+
if (now.has(id)) continue
|
|
113
|
+
const line = document.createElement('div')
|
|
114
|
+
line.className = was.owner === PLAYER ? '' : 'foe'
|
|
115
|
+
line.textContent = `${was.owner === PLAYER ? 'Your' : 'An enemy'} ${was.name.toLowerCase()} fell.`
|
|
116
|
+
$('log').prepend(line)
|
|
117
|
+
while ($('log').children.length > 9) $('log').lastElementChild!.remove()
|
|
118
|
+
}
|
|
119
|
+
known = now
|
|
120
|
+
}
|
|
121
|
+
tick()
|
|
122
|
+
const timer = setInterval(tick, 300)
|
|
123
|
+
return () => {
|
|
124
|
+
clearInterval(timer)
|
|
125
|
+
stopCard()
|
|
126
|
+
removeLayer()
|
|
127
|
+
showHud()
|
|
128
|
+
}
|
|
129
|
+
}
|