@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,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The showcases on the rules' side (`ExampleCategory`): larger scenes, each made of several parts of the engine at once.
|
|
3
|
+
* A boss fight in phases (a system that watches its health, a buff, a slam, the dead raised), a tower defense (waves on
|
|
4
|
+
* a timer, a road, lives and gold kept on the keep), two computer generals against each other (`defineBrain`), and a
|
|
5
|
+
* battle that turns back time (the world's state recorded, then laid out again as it was)
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
applyBuff,
|
|
9
|
+
bool,
|
|
10
|
+
type BuffDef,
|
|
11
|
+
defineCommand,
|
|
12
|
+
defineComponent,
|
|
13
|
+
defineIds,
|
|
14
|
+
type Entity,
|
|
15
|
+
entityDied,
|
|
16
|
+
type Game,
|
|
17
|
+
number,
|
|
18
|
+
oneOf,
|
|
19
|
+
type PlayerId,
|
|
20
|
+
type UnitTypeDef,
|
|
21
|
+
type Vec2,
|
|
22
|
+
type World,
|
|
23
|
+
exactMath
|
|
24
|
+
} from '@v12-engine/engine'
|
|
25
|
+
import { type AiModule, defineBrain } from '@v12-engine/engine/ai'
|
|
26
|
+
import { ENEMY, PLAYER, UNIT } from '../common/units'
|
|
27
|
+
import { type ExampleCategory, exampleField, tiles } from '../define'
|
|
28
|
+
|
|
29
|
+
/** The showcases' own units */
|
|
30
|
+
export const SHOW = defineIds('showcaseBoss', 'showcaseRisen', 'showcaseTower', 'showcaseRaider', 'showcaseBrute', 'showcaseKeep')
|
|
31
|
+
|
|
32
|
+
const units: UnitTypeDef[] = [
|
|
33
|
+
{
|
|
34
|
+
id: SHOW.showcaseBoss,
|
|
35
|
+
name: 'The Bone King',
|
|
36
|
+
kind: 'unit',
|
|
37
|
+
radius: 1.1,
|
|
38
|
+
stats: { health: 6000, armor: 15, attackDamage: 55, attackSpeed: 0.75, attackRange: 1.4, moveSpeed: 3 },
|
|
39
|
+
sight: 12,
|
|
40
|
+
attacks: [{ damagePoint: 0.5, acquisitionRange: 12 }]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: SHOW.showcaseRisen,
|
|
44
|
+
name: 'Risen dead',
|
|
45
|
+
kind: 'unit',
|
|
46
|
+
radius: 0.4,
|
|
47
|
+
stats: { health: 160, attackDamage: 9, attackSpeed: 1.1, attackRange: 0.5, moveSpeed: 3.6 },
|
|
48
|
+
sight: 10,
|
|
49
|
+
attacks: [{ damagePoint: 0.4, acquisitionRange: 10 }]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: SHOW.showcaseTower,
|
|
53
|
+
name: 'Arrow tower',
|
|
54
|
+
kind: 'unit',
|
|
55
|
+
radius: 0.9,
|
|
56
|
+
stats: { health: 1500, attackDamage: 24, attackSpeed: 1.3, attackRange: 7.5, moveSpeed: 0, armor: 20 },
|
|
57
|
+
sight: 9,
|
|
58
|
+
attacks: [{ damagePoint: 0.2, acquisitionRange: 7.5, projectileSpeed: 26 }]
|
|
59
|
+
},
|
|
60
|
+
// the raiders walk the road and fight nothing: they only want to get through
|
|
61
|
+
{ id: SHOW.showcaseRaider, name: 'Raider', kind: 'unit', radius: 0.4, stats: { health: 110, moveSpeed: 2.8 }, sight: 4 },
|
|
62
|
+
{ id: SHOW.showcaseBrute, name: 'Brute', kind: 'unit', radius: 0.55, stats: { health: 520, armor: 20, moveSpeed: 1.9 }, sight: 4 },
|
|
63
|
+
{ id: SHOW.showcaseKeep, name: 'Keep', kind: 'unit', radius: 2, stats: { health: 5000, moveSpeed: 0 }, sight: 8 }
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
/** Every unit of the world taken away */
|
|
67
|
+
const clearAll = (world: World) => {
|
|
68
|
+
for (const unit of [...world.units.values()]) if (unit.alive) world.kill(unit, undefined, false, 'removed')
|
|
69
|
+
}
|
|
70
|
+
/** The alive units of the world a test says yes to */
|
|
71
|
+
const alive = (world: World, test: (unit: Entity) => boolean) => [...world.units.values()].filter((unit) => unit.alive && test(unit))
|
|
72
|
+
/** One order to many, the way a player gives it */
|
|
73
|
+
const orderAll = (world: World, player: PlayerId, units: readonly Entity[], order: { type: string; point?: Vec2; target?: number }) => {
|
|
74
|
+
if (units.length) world.issue({ type: 'order', player, entities: units.map((unit) => unit.id), order: { ...order, formation: 'none' } as never })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- the boss: phases by its health
|
|
78
|
+
|
|
79
|
+
/** Where the boss fight stands, kept on the boss and told to every screen: its phase, and when it slams next */
|
|
80
|
+
export const bossState = defineComponent<{ phase: 1 | 2 | 3; slamAt: number }>({
|
|
81
|
+
id: 'showcase.bossFight',
|
|
82
|
+
default: () => ({ phase: 1, slamAt: 0 }),
|
|
83
|
+
replicate: 'everyone'
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/** The fury of the boss's last phase: faster blows, faster feet */
|
|
87
|
+
const enraged: BuffDef = { id: 'showcase.enraged', name: 'Enraged', percent: { attackSpeed: 0.7, moveSpeed: 0.35, attackDamage: 0.25 } }
|
|
88
|
+
|
|
89
|
+
/** The party: a knight, footmen, archers and a mage, south of the boss */
|
|
90
|
+
function party(world: World): void {
|
|
91
|
+
world.spawn(UNIT.knight, { at: { x: 24, z: 15 }, owner: PLAYER, facing: 0 })
|
|
92
|
+
for (let i = 0; i < 5; i++) world.spawn(UNIT.footman, { at: { x: 20 + i * 2, z: 16.5 }, owner: PLAYER, facing: 0 })
|
|
93
|
+
for (let i = 0; i < 4; i++) world.spawn(UNIT.archer, { at: { x: 20.5 + i * 2.3, z: 12.5 }, owner: PLAYER, facing: 0 })
|
|
94
|
+
for (let i = 0; i < 2; i++) world.spawn(UNIT.mage, { at: { x: 22 + i * 4, z: 11 }, owner: PLAYER, facing: 0 })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The boss fight laid out: the party to the south, the boss on its dais to the north */
|
|
98
|
+
function bossArena(world: World): void {
|
|
99
|
+
party(world)
|
|
100
|
+
const boss = world.spawn(SHOW.showcaseBoss, { at: { x: 24, z: 31 }, owner: ENEMY, facing: Math.PI })
|
|
101
|
+
boss.set(bossState, { phase: 1, slamAt: 0 })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Raises the dead round the boss: `count` of them out of the ground, in a ring */
|
|
105
|
+
function raiseDead(world: World, at: Vec2, count: number): void {
|
|
106
|
+
for (let i = 0; i < count; i++) {
|
|
107
|
+
const angle = (i / count) * Math.PI * 2
|
|
108
|
+
const point = { x: at.x + exactMath.cos(angle) * 3, z: at.z + exactMath.sin(angle) * 3 }
|
|
109
|
+
world.spawn(SHOW.showcaseRisen, { at: point, owner: ENEMY, facing: angle })
|
|
110
|
+
world.fx('dust', { at: point })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The boss's phases, watched a few times a second: at two thirds of its health it roars and raises the dead; at a
|
|
116
|
+
* third it is enraged (`enraged`), raises more, and slams the ground every few seconds, hurting all round it
|
|
117
|
+
*/
|
|
118
|
+
const bossPhases = {
|
|
119
|
+
name: 'showcase.bossPhases',
|
|
120
|
+
every: 3,
|
|
121
|
+
update: (world: World) => {
|
|
122
|
+
for (const boss of world.units.values()) {
|
|
123
|
+
if (!boss.alive || boss.def.id !== SHOW.showcaseBoss) continue
|
|
124
|
+
const fight = boss.get(bossState) ?? { phase: 1, slamAt: 0 }
|
|
125
|
+
const share = boss.health / boss.maxHealth
|
|
126
|
+
if (fight.phase === 1 && share < 2 / 3) {
|
|
127
|
+
boss.set(bossState, { ...fight, phase: 2 })
|
|
128
|
+
world.fx('shockwave', { at: boss.pos, params: { size: 2 } })
|
|
129
|
+
raiseDead(world, boss.pos, 6)
|
|
130
|
+
world.hint(PLAYER, 'The Bone King calls up his dead!')
|
|
131
|
+
} else if (fight.phase === 2 && share < 1 / 3) {
|
|
132
|
+
boss.set(bossState, { phase: 3, slamAt: world.time + 2 })
|
|
133
|
+
world.fx('explosion', { at: boss.pos, params: { size: 1.4 } })
|
|
134
|
+
applyBuff(world, boss, enraged.id, 600, boss)
|
|
135
|
+
raiseDead(world, boss.pos, 5)
|
|
136
|
+
world.hint(PLAYER, 'The Bone King is enraged!')
|
|
137
|
+
} else if (fight.phase === 3 && world.time >= fight.slamAt) {
|
|
138
|
+
boss.set(bossState, { ...fight, slamAt: world.time + 5 })
|
|
139
|
+
// the slam: all of the other side within 4 of it hurt, more the nearer they stand
|
|
140
|
+
world.fx('ground-crack', { at: boss.pos, params: { size: 1.6 } })
|
|
141
|
+
world.fx('shockwave', { at: boss.pos, params: { size: 1.5 } })
|
|
142
|
+
for (const unit of world.units.values()) {
|
|
143
|
+
if (!unit.alive || unit.owner !== PLAYER) continue
|
|
144
|
+
const away = exactMath.hypot(unit.pos.x - boss.pos.x, unit.pos.z - boss.pos.z)
|
|
145
|
+
if (away < 4) world.damage(unit, 90 * (1 - away / 5), boss)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** The fight again from the start */
|
|
152
|
+
export const bossReset = defineCommand('showcase.bossReset', {}, (world) => {
|
|
153
|
+
clearAll(world)
|
|
154
|
+
bossArena(world)
|
|
155
|
+
})
|
|
156
|
+
/** The party sent at the boss, fighting what is in the way */
|
|
157
|
+
export const bossCharge = defineCommand('showcase.bossCharge', {}, (world) => {
|
|
158
|
+
orderAll(
|
|
159
|
+
world,
|
|
160
|
+
PLAYER,
|
|
161
|
+
alive(world, (unit) => unit.owner === PLAYER),
|
|
162
|
+
{ type: 'attackMove', point: { x: 24, z: 33 } }
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// ---- the tower defense: a road, waves, towers, lives and gold
|
|
167
|
+
|
|
168
|
+
/** The road the raiders walk, from the west to the keep in the east */
|
|
169
|
+
export const ROAD: Vec2[] = [
|
|
170
|
+
{ x: 1, z: 38 },
|
|
171
|
+
{ x: 14, z: 38 },
|
|
172
|
+
{ x: 14, z: 22 },
|
|
173
|
+
{ x: 32, z: 22 },
|
|
174
|
+
{ x: 32, z: 10 },
|
|
175
|
+
{ x: 42, z: 10 }
|
|
176
|
+
]
|
|
177
|
+
/** Where towers may stand, along the road, in the order they are built */
|
|
178
|
+
const SPOTS: Vec2[] = [
|
|
179
|
+
{ x: 17.5, z: 30 },
|
|
180
|
+
{ x: 10.5, z: 30 },
|
|
181
|
+
{ x: 23, z: 25.5 },
|
|
182
|
+
{ x: 23, z: 18.5 },
|
|
183
|
+
{ x: 28.5, z: 15 },
|
|
184
|
+
{ x: 35.5, z: 15 },
|
|
185
|
+
{ x: 8, z: 41.5 },
|
|
186
|
+
{ x: 17.5, z: 41.5 },
|
|
187
|
+
{ x: 35.5, z: 6.5 },
|
|
188
|
+
{ x: 28.5, z: 6.5 }
|
|
189
|
+
]
|
|
190
|
+
/** A time that never comes: a wave not yet beaten has its next one here (the state is sent as plain data, which has no Infinity) */
|
|
191
|
+
const NEVER = 1e9
|
|
192
|
+
/** What a tower costs, and what a raider and a brute are worth */
|
|
193
|
+
export const TOWER_COST = 60
|
|
194
|
+
const BOUNTY = { [SHOW.showcaseRaider]: 6, [SHOW.showcaseBrute]: 20 } as Record<string, number>
|
|
195
|
+
|
|
196
|
+
/** How the defense stands, kept on the keep and told to every screen */
|
|
197
|
+
interface Defense {
|
|
198
|
+
wave: number
|
|
199
|
+
lives: number
|
|
200
|
+
gold: number
|
|
201
|
+
/** Raiders still to come out this wave, and when the next does */
|
|
202
|
+
toCome: number
|
|
203
|
+
nextAt: number
|
|
204
|
+
/** When the next wave starts, once this one is beaten */
|
|
205
|
+
waveAt: number
|
|
206
|
+
}
|
|
207
|
+
export const defenseState = defineComponent<Defense>({
|
|
208
|
+
id: 'showcase.defense',
|
|
209
|
+
default: () => ({ wave: 0, lives: 20, gold: 120, toCome: 0, nextAt: 0, waveAt: 3 }),
|
|
210
|
+
replicate: 'everyone'
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
/** The keep, where the defense is kept */
|
|
214
|
+
const keepOf = (world: World) => alive(world, (unit) => unit.def.id === SHOW.showcaseKeep)[0]
|
|
215
|
+
|
|
216
|
+
/** The map of the tower defense: the field with the road painted on it */
|
|
217
|
+
function defenseMap() {
|
|
218
|
+
const map = exampleField({ name: 'Hold the road', players: 2 })
|
|
219
|
+
const road = tiles.findIndex((tile) => tile.id === 'road') + 1 || tiles.findIndex((tile) => tile.id === 'dirt') + 1
|
|
220
|
+
for (let i = 0; i + 1 < ROAD.length; i++) {
|
|
221
|
+
const a = ROAD[i]!
|
|
222
|
+
const b = ROAD[i + 1]!
|
|
223
|
+
for (let x = Math.min(a.x, b.x) - 1; x <= Math.max(a.x, b.x) + 1; x++)
|
|
224
|
+
for (let z = Math.min(a.z, b.z) - 1; z <= Math.max(a.z, b.z) + 1; z++)
|
|
225
|
+
if (x >= 0 && z >= 0 && x < map.width && z < map.depth) map.tiles[Math.floor(z) * map.width + Math.floor(x)] = road
|
|
226
|
+
}
|
|
227
|
+
return map
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** How far along the road a point is, as the leg it is on (0 the first), to send it on to the next corner */
|
|
231
|
+
function legOf(point: Vec2): number {
|
|
232
|
+
let best = 0
|
|
233
|
+
let nearest = Infinity
|
|
234
|
+
for (let i = 0; i + 1 < ROAD.length; i++) {
|
|
235
|
+
const a = ROAD[i]!
|
|
236
|
+
const b = ROAD[i + 1]!
|
|
237
|
+
const dx = b.x - a.x
|
|
238
|
+
const dz = b.z - a.z
|
|
239
|
+
const t = Math.max(0, Math.min(1, ((point.x - a.x) * dx + (point.z - a.z) * dz) / (dx * dx + dz * dz)))
|
|
240
|
+
const away = exactMath.hypot(a.x + dx * t - point.x, a.z + dz * t - point.z)
|
|
241
|
+
if (away < nearest - 0.01) {
|
|
242
|
+
nearest = away
|
|
243
|
+
best = i
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return best
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The defense laid out: the keep at the road's end, two towers to start with */
|
|
250
|
+
function defenseStart(world: World): void {
|
|
251
|
+
const keep = world.spawn(SHOW.showcaseKeep, { at: { x: 44.5, z: 10 }, owner: PLAYER, facing: -Math.PI / 2 })
|
|
252
|
+
keep.set(defenseState, { wave: 0, lives: 20, gold: 120, toCome: 0, nextAt: 0, waveAt: world.time + 4 })
|
|
253
|
+
for (const spot of SPOTS.slice(0, 2)) world.spawn(SHOW.showcaseTower, { at: spot, owner: PLAYER })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The waves, ten times a second: a raider out every so often while the wave has some to come; the next wave a few
|
|
258
|
+
* seconds after the last is beaten, bigger, every third with brutes. Each raider is sent on to the next corner of the
|
|
259
|
+
* road; one that gets to the keep is taken away, and a life with it
|
|
260
|
+
*/
|
|
261
|
+
const waves = {
|
|
262
|
+
name: 'showcase.waves',
|
|
263
|
+
every: 3,
|
|
264
|
+
update: (world: World) => {
|
|
265
|
+
const keep = keepOf(world)
|
|
266
|
+
const defense = keep?.get(defenseState)
|
|
267
|
+
if (!keep || !defense) return
|
|
268
|
+
const raiders = alive(world, (unit) => unit.owner === ENEMY && (unit.def.id === SHOW.showcaseRaider || unit.def.id === SHOW.showcaseBrute))
|
|
269
|
+
const now = { ...defense }
|
|
270
|
+
if (now.lives > 0 && now.toCome > 0 && world.time >= now.nextAt) {
|
|
271
|
+
const brute = now.wave % 3 === 0 && now.toCome % 3 === 0
|
|
272
|
+
world.spawn(brute ? SHOW.showcaseBrute : SHOW.showcaseRaider, { at: ROAD[0]!, owner: ENEMY, facing: Math.PI / 2 })
|
|
273
|
+
now.toCome--
|
|
274
|
+
now.nextAt = world.time + (brute ? 1.6 : 0.8)
|
|
275
|
+
} else if (now.lives > 0 && !now.toCome && !raiders.length && world.time >= now.waveAt) {
|
|
276
|
+
now.wave++
|
|
277
|
+
now.toCome = 6 + now.wave * 3
|
|
278
|
+
now.nextAt = world.time
|
|
279
|
+
now.waveAt = NEVER
|
|
280
|
+
world.hint(PLAYER, `Wave ${now.wave}`)
|
|
281
|
+
} else if (!now.toCome && !raiders.length && now.waveAt === NEVER) now.waveAt = world.time + 5
|
|
282
|
+
const end = ROAD[ROAD.length - 1]!
|
|
283
|
+
for (const raider of raiders) {
|
|
284
|
+
if (exactMath.hypot(raider.pos.x - end.x, raider.pos.z - end.z) < 1.5) {
|
|
285
|
+
world.kill(raider, undefined, false, 'removed')
|
|
286
|
+
world.fx('impact-ring', { at: end })
|
|
287
|
+
now.lives = Math.max(0, now.lives - (raider.def.id === SHOW.showcaseBrute ? 3 : 1))
|
|
288
|
+
if (!now.lives) world.hint(PLAYER, 'The keep has fallen. Start again to hold the road anew.')
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
const next = ROAD[legOf(raider.pos) + 1]!
|
|
292
|
+
const going = raider.orders[0]?.init.point as Vec2 | undefined
|
|
293
|
+
if (!going || going.x !== next.x || going.z !== next.z) raider.order('move', next)
|
|
294
|
+
}
|
|
295
|
+
keep.set(defenseState, now)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** A tower built at the next free spot, if the gold is there */
|
|
300
|
+
export const buildTower = defineCommand('showcase.buildTower', {}, (world, player) => {
|
|
301
|
+
const keep = keepOf(world)
|
|
302
|
+
const defense = keep?.get(defenseState)
|
|
303
|
+
if (!keep || !defense) return
|
|
304
|
+
if (defense.gold < TOWER_COST) return world.message(player, 'Not enough gold')
|
|
305
|
+
const towers = alive(world, (unit) => unit.def.id === SHOW.showcaseTower)
|
|
306
|
+
const spot = SPOTS.find((each) => !towers.some((tower) => exactMath.hypot(tower.pos.x - each.x, tower.pos.z - each.z) < 1))
|
|
307
|
+
if (!spot) return world.message(player, 'Every spot has a tower')
|
|
308
|
+
world.spawn(SHOW.showcaseTower, { at: spot, owner: player })
|
|
309
|
+
world.fx('dust', { at: spot })
|
|
310
|
+
keep.set(defenseState, { ...defense, gold: defense.gold - TOWER_COST })
|
|
311
|
+
})
|
|
312
|
+
/** The road held again from the first wave */
|
|
313
|
+
export const defenseReset = defineCommand('showcase.defenseReset', {}, (world) => {
|
|
314
|
+
clearAll(world)
|
|
315
|
+
defenseStart(world)
|
|
316
|
+
})
|
|
317
|
+
/** The next wave now, for gold */
|
|
318
|
+
export const callWave = defineCommand('showcase.callWave', {}, (world) => {
|
|
319
|
+
const keep = keepOf(world)
|
|
320
|
+
const defense = keep?.get(defenseState)
|
|
321
|
+
if (keep && defense && defense.waveAt !== NEVER) keep.set(defenseState, { ...defense, waveAt: world.time, gold: defense.gold + 15 })
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// ---- two generals: a computer on each side, with tactics of its own
|
|
325
|
+
|
|
326
|
+
/** The first of a set a test likes best, by a score: the lowest wins */
|
|
327
|
+
const best = <T>(list: readonly T[], score: (each: T) => number): T | undefined => {
|
|
328
|
+
let found: T | undefined
|
|
329
|
+
let low = Infinity
|
|
330
|
+
for (const each of list) {
|
|
331
|
+
const s = score(each)
|
|
332
|
+
if (s < low) {
|
|
333
|
+
low = s
|
|
334
|
+
found = each
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return found
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A general's tactics, a module of a computer's brain (`AiModule`): it reads the game as its seat sees it (`ai.view`)
|
|
342
|
+
* and orders its army as a player would (`ai.send`). Footmen hit the weakest foe near the front together, and fall
|
|
343
|
+
* back to the archers when badly hurt; archers keep out of a blade's reach and shoot the weakest in range; the knight
|
|
344
|
+
* goes round the side for the archers. An order is given again only when it changes, so swings are not cut off
|
|
345
|
+
*/
|
|
346
|
+
const tactics = (): AiModule => {
|
|
347
|
+
const given = new Map<number, string>()
|
|
348
|
+
return {
|
|
349
|
+
id: 'showcase.tactics',
|
|
350
|
+
think(ai) {
|
|
351
|
+
const mine = ai.view.mine.units().filter((each) => each.health > 0)
|
|
352
|
+
const foes = ai.view.enemies.units().filter((each) => each.health > 0 && each.visible)
|
|
353
|
+
const order = (id: number, key: string, command: { type: string; point?: Vec2; target?: number }) => {
|
|
354
|
+
if (given.get(id) === key) return
|
|
355
|
+
given.set(id, key)
|
|
356
|
+
ai.send({ type: 'order', entities: [id], order: command } as never)
|
|
357
|
+
}
|
|
358
|
+
/** The foe a unit was set on last, while it lives and is still within `reach`: kept, so a swing is never cut off for another */
|
|
359
|
+
const still = (unit: { id: number; x: number; z: number }, reach: number) => {
|
|
360
|
+
const was = Number(given.get(unit.id)?.replace(/^\D+/, ''))
|
|
361
|
+
const foe = foes.find((each) => each.id === was)
|
|
362
|
+
return foe && exactMath.hypot(foe.x - unit.x, foe.z - unit.z) < reach ? foe : undefined
|
|
363
|
+
}
|
|
364
|
+
const front = ai.view.enemies.centre() ?? { x: 32, z: 32 }
|
|
365
|
+
const home = ai.view.mine.centre() ?? front
|
|
366
|
+
const archers = mine.filter((each) => each.type === UNIT.archer)
|
|
367
|
+
const rear = archers.length
|
|
368
|
+
? { x: archers.reduce((s, a) => s + a.x, 0) / archers.length, z: archers.reduce((s, a) => s + a.z, 0) / archers.length }
|
|
369
|
+
: home
|
|
370
|
+
for (const unit of mine) {
|
|
371
|
+
const hurt = unit.health / (unit.maxHealth ?? unit.health) < 0.3
|
|
372
|
+
const near = best(foes, (foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z))
|
|
373
|
+
const nearAway = near ? exactMath.hypot(near.x - unit.x, near.z - unit.z) : Infinity
|
|
374
|
+
if (unit.type === UNIT.archer) {
|
|
375
|
+
// a blade too near: step back, away from it
|
|
376
|
+
if (near && nearAway < 3 && near.type !== UNIT.archer) {
|
|
377
|
+
const dx = unit.x - near.x
|
|
378
|
+
const dz = unit.z - near.z
|
|
379
|
+
const k = 4 / (exactMath.hypot(dx, dz) || 1)
|
|
380
|
+
order(unit.id, `back${Math.round(unit.x)},${Math.round(unit.z)}`, { type: 'move', point: { x: unit.x + dx * k, z: unit.z + dz * k } })
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
const target =
|
|
384
|
+
still(unit, 8) ??
|
|
385
|
+
best(
|
|
386
|
+
foes.filter((foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z) < 8),
|
|
387
|
+
(foe) => foe.health
|
|
388
|
+
)
|
|
389
|
+
if (target) order(unit.id, `shoot${target.id}`, { type: 'attack', target: target.id })
|
|
390
|
+
else order(unit.id, 'advance', { type: 'attackMove', point: front })
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
if (unit.type === UNIT.knight) {
|
|
394
|
+
const hunted = still(unit, 30)
|
|
395
|
+
const prey =
|
|
396
|
+
(hunted?.type === UNIT.archer ? hunted : undefined) ??
|
|
397
|
+
best(
|
|
398
|
+
foes.filter((foe) => foe.type === UNIT.archer),
|
|
399
|
+
(foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z)
|
|
400
|
+
)
|
|
401
|
+
if (prey) order(unit.id, `hunt${prey.id}`, { type: 'attack', target: prey.id })
|
|
402
|
+
else order(unit.id, 'advance', { type: 'attackMove', point: front })
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
if (hurt && nearAway < 6) {
|
|
406
|
+
order(unit.id, 'fall back', { type: 'move', point: rear })
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
// the footmen together on the weakest foe near them
|
|
410
|
+
const target =
|
|
411
|
+
still(unit, 7) ??
|
|
412
|
+
best(
|
|
413
|
+
foes.filter((foe) => exactMath.hypot(foe.x - unit.x, foe.z - unit.z) < 7),
|
|
414
|
+
(foe) => foe.health + exactMath.hypot(foe.x - unit.x, foe.z - unit.z) * 10
|
|
415
|
+
)
|
|
416
|
+
if (target) order(unit.id, `hit${target.id}`, { type: 'attack', target: target.id })
|
|
417
|
+
else order(unit.id, 'advance', { type: 'attackMove', point: front })
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The general: a brain of one module, thinking twice a second */
|
|
424
|
+
export const general = defineBrain({ id: 'showcase.general', modules: [tactics], every: 0.5 })
|
|
425
|
+
|
|
426
|
+
/** The two armies of the generals: footmen in front, archers behind, a knight on a wing */
|
|
427
|
+
function generalsArmies(world: World): void {
|
|
428
|
+
for (const [owner, z, back] of [
|
|
429
|
+
[PLAYER, 18, -1],
|
|
430
|
+
[ENEMY, 46, 1]
|
|
431
|
+
] as const) {
|
|
432
|
+
for (let i = 0; i < 14; i++)
|
|
433
|
+
world.spawn(UNIT.footman, { at: { x: 23 + (i % 7) * 1.5, z: z + Math.floor(i / 7) * 1.4 * back }, owner, facing: back < 0 ? 0 : Math.PI })
|
|
434
|
+
for (let i = 0; i < 8; i++)
|
|
435
|
+
world.spawn(UNIT.archer, {
|
|
436
|
+
at: { x: 24.5 + (i % 4) * 2.2, z: z + (3.6 + Math.floor(i / 4) * 1.4) * back },
|
|
437
|
+
owner,
|
|
438
|
+
facing: back < 0 ? 0 : Math.PI
|
|
439
|
+
})
|
|
440
|
+
world.spawn(UNIT.knight, { at: { x: owner === PLAYER ? 18 : 46, z: z + 1.5 * back }, owner, facing: back < 0 ? 0 : Math.PI })
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Both generals seated, each playing its side */
|
|
445
|
+
function seatGenerals(world: World): void {
|
|
446
|
+
world.computers.seat(PLAYER, { brain: general })
|
|
447
|
+
world.computers.seat(ENEMY, { brain: general })
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Both armies laid out again, and the generals at them */
|
|
451
|
+
export const generalsReset = defineCommand('showcase.generalsReset', {}, (world) => {
|
|
452
|
+
clearAll(world)
|
|
453
|
+
generalsArmies(world)
|
|
454
|
+
seatGenerals(world)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// ---- a battle that turns back time: the world recorded, and laid out again as it was
|
|
458
|
+
|
|
459
|
+
/** One unit as it was at a moment: enough to make it again */
|
|
460
|
+
interface Kept {
|
|
461
|
+
type: string
|
|
462
|
+
owner: PlayerId
|
|
463
|
+
x: number
|
|
464
|
+
z: number
|
|
465
|
+
facing: number
|
|
466
|
+
health: number
|
|
467
|
+
}
|
|
468
|
+
/** What the world looked like, twice a second for the last fifteen seconds: kept in the world's state, so a save has it */
|
|
469
|
+
const historyState = defineComponent<{ frames: { time: number; units: Kept[] }[] }>({ id: 'showcase.history', default: () => ({ frames: [] }) })
|
|
470
|
+
|
|
471
|
+
/** The armies of the rewind battle */
|
|
472
|
+
function rewindArmies(world: World): void {
|
|
473
|
+
for (const [owner, z, back] of [
|
|
474
|
+
[PLAYER, 18, -1],
|
|
475
|
+
[ENEMY, 30, 1]
|
|
476
|
+
] as const) {
|
|
477
|
+
for (let i = 0; i < 12; i++)
|
|
478
|
+
world.spawn(UNIT.footman, { at: { x: 20 + (i % 6) * 1.6, z: z + Math.floor(i / 6) * 1.4 * back }, owner, facing: back < 0 ? 0 : Math.PI })
|
|
479
|
+
for (let i = 0; i < 6; i++) world.spawn(UNIT.archer, { at: { x: 21 + (i % 6) * 1.4, z: z + 4 * back }, owner, facing: back < 0 ? 0 : Math.PI })
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Both sides at each other */
|
|
484
|
+
function fight(world: World): void {
|
|
485
|
+
for (const owner of [PLAYER, ENEMY])
|
|
486
|
+
orderAll(
|
|
487
|
+
world,
|
|
488
|
+
owner,
|
|
489
|
+
alive(world, (unit) => unit.owner === owner),
|
|
490
|
+
{ type: 'attackMove', point: { x: 24, z: owner === PLAYER ? 36 : 12 } }
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The world recorded twice a second, on the rewind example's field alone */
|
|
495
|
+
const recording = {
|
|
496
|
+
name: 'showcase.recording',
|
|
497
|
+
every: 15,
|
|
498
|
+
update: (world: World) => {
|
|
499
|
+
if (world.map?.name !== 'Turn back time') return
|
|
500
|
+
const history = world.state(historyState)
|
|
501
|
+
const units = alive(world, () => true).map((unit) => ({
|
|
502
|
+
type: unit.def.id,
|
|
503
|
+
owner: unit.owner,
|
|
504
|
+
x: unit.pos.x,
|
|
505
|
+
z: unit.pos.z,
|
|
506
|
+
facing: unit.facing,
|
|
507
|
+
health: unit.health
|
|
508
|
+
}))
|
|
509
|
+
world.setState(historyState, { frames: [...history.frames.slice(-29), { time: world.time, units }] })
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Time turned back `seconds`: every unit taken away and laid out again as the record of that moment has it, where it
|
|
515
|
+
* stood, as hurt as it was, the fallen of since standing again; then the fight goes on from there
|
|
516
|
+
*/
|
|
517
|
+
export const rewind = defineCommand('showcase.rewind', { seconds: number }, (world, _player, { seconds }) => {
|
|
518
|
+
const { frames } = world.state(historyState)
|
|
519
|
+
const then = best(frames, (frame) => Math.abs(frame.time - (world.time - seconds)))
|
|
520
|
+
if (!then) return
|
|
521
|
+
clearAll(world)
|
|
522
|
+
for (const kept of then.units) {
|
|
523
|
+
const unit = world.spawn(kept.type, { at: { x: kept.x, z: kept.z }, owner: kept.owner, facing: kept.facing })
|
|
524
|
+
unit.health = Math.min(kept.health, unit.maxHealth)
|
|
525
|
+
}
|
|
526
|
+
world.setState(historyState, { frames: frames.filter((frame) => frame.time <= then.time) })
|
|
527
|
+
world.fx('shockwave', { at: { x: 24, z: 24 }, params: { size: 3, color: '#9fd6ff' } })
|
|
528
|
+
fight(world)
|
|
529
|
+
})
|
|
530
|
+
/** The battle from the start */
|
|
531
|
+
export const rewindReset = defineCommand('showcase.rewindReset', {}, (world) => {
|
|
532
|
+
clearAll(world)
|
|
533
|
+
world.setState(historyState, { frames: [] })
|
|
534
|
+
rewindArmies(world)
|
|
535
|
+
})
|
|
536
|
+
/** The two sides sent at each other */
|
|
537
|
+
export const rewindFight = defineCommand('showcase.rewindFight', {}, (world) => fight(world))
|
|
538
|
+
|
|
539
|
+
// ---- a march: companies in formation behind a leader who walks a road round the field
|
|
540
|
+
|
|
541
|
+
/** The leader's road: round the field and back, for ever */
|
|
542
|
+
export const MARCH: Vec2[] = [
|
|
543
|
+
{ x: 16, z: 16 },
|
|
544
|
+
{ x: 48, z: 16 },
|
|
545
|
+
{ x: 52, z: 32 },
|
|
546
|
+
{ x: 48, z: 48 },
|
|
547
|
+
{ x: 16, z: 48 },
|
|
548
|
+
{ x: 12, z: 32 }
|
|
549
|
+
]
|
|
550
|
+
/** How the companies march, kept in the world's state: their shape, and whether they march or stand */
|
|
551
|
+
export const marching = defineComponent<{ shape: 'rows' | 'square' | 'circle'; halted: boolean }>({
|
|
552
|
+
id: 'showcase.marching',
|
|
553
|
+
default: () => ({ shape: 'rows', halted: false })
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
/** The companies, by what they are, and how far behind the leader each marches */
|
|
557
|
+
const COMPANIES = [
|
|
558
|
+
{ type: UNIT.footman, count: 36, behind: 5 },
|
|
559
|
+
{ type: UNIT.archer, count: 24, behind: 12 },
|
|
560
|
+
{ type: UNIT.mage, count: 12, behind: 18 }
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
/** The leader, and the companies in ranks behind it */
|
|
564
|
+
function column(world: World): void {
|
|
565
|
+
world.spawn(UNIT.knight, { at: MARCH[0]!, owner: PLAYER, facing: Math.PI / 2 })
|
|
566
|
+
for (const company of COMPANIES)
|
|
567
|
+
for (let i = 0; i < company.count; i++)
|
|
568
|
+
world.spawn(company.type, {
|
|
569
|
+
at: { x: MARCH[0]!.x - company.behind - Math.floor(i / 6) * 1.4, z: MARCH[0]!.z - 3.5 + (i % 6) * 1.4 },
|
|
570
|
+
owner: PLAYER,
|
|
571
|
+
facing: Math.PI / 2
|
|
572
|
+
})
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The march, every two seconds: the leader walks on to the next point of its road; each company is sent, in one order and in
|
|
577
|
+
* the formation chosen, to a place behind the leader on the way it goes, facing that way (`OrderInit.facing`). The
|
|
578
|
+
* formation lays each company out anew round its place; the units find their spots in it as they walk
|
|
579
|
+
*/
|
|
580
|
+
const march = {
|
|
581
|
+
name: 'showcase.march',
|
|
582
|
+
every: 60,
|
|
583
|
+
update: (world: World) => {
|
|
584
|
+
if (world.map?.name !== 'March') return
|
|
585
|
+
const leader = alive(world, (unit) => unit.def.id === UNIT.knight)[0]
|
|
586
|
+
if (!leader) return
|
|
587
|
+
const how = world.state(marching)
|
|
588
|
+
if (how.halted) return
|
|
589
|
+
const goal = leader.orders[0]?.init.point as Vec2 | undefined
|
|
590
|
+
if (!goal || exactMath.hypot(goal.x - leader.pos.x, goal.z - leader.pos.z) < 3) {
|
|
591
|
+
const near = MARCH.reduce(
|
|
592
|
+
(best, each, i) =>
|
|
593
|
+
exactMath.hypot(each.x - leader.pos.x, each.z - leader.pos.z) <
|
|
594
|
+
exactMath.hypot(MARCH[best]!.x - leader.pos.x, MARCH[best]!.z - leader.pos.z)
|
|
595
|
+
? i
|
|
596
|
+
: best,
|
|
597
|
+
0
|
|
598
|
+
)
|
|
599
|
+
leader.order('move', MARCH[(near + 1) % MARCH.length]!)
|
|
600
|
+
}
|
|
601
|
+
const to = (leader.orders[0]?.init.point as Vec2 | undefined) ?? leader.pos
|
|
602
|
+
const away = exactMath.hypot(to.x - leader.pos.x, to.z - leader.pos.z) || 1
|
|
603
|
+
const forward = { x: (to.x - leader.pos.x) / away, z: (to.z - leader.pos.z) / away }
|
|
604
|
+
const facing = exactMath.atan2(forward.x, forward.z)
|
|
605
|
+
for (const company of COMPANIES) {
|
|
606
|
+
const units = alive(world, (unit) => unit.def.id === company.type)
|
|
607
|
+
const place = { x: leader.pos.x - forward.x * company.behind, z: leader.pos.z - forward.z * company.behind }
|
|
608
|
+
if (units.length)
|
|
609
|
+
world.issue({
|
|
610
|
+
type: 'order',
|
|
611
|
+
player: PLAYER,
|
|
612
|
+
entities: units.map((unit) => unit.id),
|
|
613
|
+
order: { type: 'move', point: place, formation: how.shape, facing } as never
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** The companies' shape, or a halt and on again */
|
|
620
|
+
export const marchAs = defineCommand('showcase.marchAs', { shape: oneOf('rows', 'square', 'circle') }, (world, _player, { shape }) => {
|
|
621
|
+
world.setState(marching, { ...world.state(marching), shape })
|
|
622
|
+
})
|
|
623
|
+
export const halt = defineCommand('showcase.halt', { halted: bool }, (world, player, { halted }) => {
|
|
624
|
+
world.setState(marching, { ...world.state(marching), halted })
|
|
625
|
+
if (halted)
|
|
626
|
+
orderAll(
|
|
627
|
+
world,
|
|
628
|
+
player,
|
|
629
|
+
alive(world, (unit) => unit.owner === player),
|
|
630
|
+
{ type: 'stop' }
|
|
631
|
+
)
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
export const category: ExampleCategory = {
|
|
635
|
+
rules: (game: Game<any>) => {
|
|
636
|
+
for (const unit of units) game.unit(unit)
|
|
637
|
+
for (const component of [bossState, defenseState, historyState, marching]) game.component(component)
|
|
638
|
+
game.buff(enraged)
|
|
639
|
+
game.brain(general)
|
|
640
|
+
game.system(bossPhases)
|
|
641
|
+
game.system(waves)
|
|
642
|
+
game.system(recording)
|
|
643
|
+
game.system(march)
|
|
644
|
+
for (const command of [bossReset, bossCharge, buildTower, defenseReset, callWave, generalsReset, rewind, rewindReset, rewindFight, marchAs, halt])
|
|
645
|
+
game.command(command)
|
|
646
|
+
// a raider's bounty, to the keep's gold
|
|
647
|
+
game.on(entityDied, (world, { entity, reason }) => {
|
|
648
|
+
const bounty = BOUNTY[entity.def.id]
|
|
649
|
+
const keep = bounty && reason !== 'removed' ? keepOf(world) : undefined
|
|
650
|
+
const defense = keep?.get(defenseState)
|
|
651
|
+
if (keep && defense) keep.set(defenseState, { ...defense, gold: defense.gold + bounty! })
|
|
652
|
+
})
|
|
653
|
+
},
|
|
654
|
+
examples: [
|
|
655
|
+
{
|
|
656
|
+
id: 'showcase-boss',
|
|
657
|
+
category: 'showcase',
|
|
658
|
+
title: 'The Bone King: a boss in phases',
|
|
659
|
+
summary:
|
|
660
|
+
'A party against a boss that changes as it is hurt: at two thirds it raises the dead, at a third it is enraged and slams the ground. Charge, or lead the party yourself (select, right-click).',
|
|
661
|
+
tags: ['boss', 'phases', 'raid', 'adds', 'enrage', 'system', 'buff', 'fight'],
|
|
662
|
+
map: () => exampleField({ name: 'Bone King', players: 2 }),
|
|
663
|
+
start: (world) => bossArena(world),
|
|
664
|
+
source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
id: 'showcase-defense',
|
|
668
|
+
category: 'showcase',
|
|
669
|
+
title: 'Hold the road: a tower defense',
|
|
670
|
+
summary:
|
|
671
|
+
'Raiders come down the road in waves, bigger each time, brutes every third; towers shoot them as they pass. Build towers with the gold they bring, and keep them from the keep.',
|
|
672
|
+
tags: ['tower defense', 'waves', 'road', 'lives', 'gold', 'towers', 'td'],
|
|
673
|
+
map: () => defenseMap(),
|
|
674
|
+
start: (world) => defenseStart(world),
|
|
675
|
+
source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
id: 'showcase-generals',
|
|
679
|
+
category: 'showcase',
|
|
680
|
+
title: 'Two computer generals',
|
|
681
|
+
summary:
|
|
682
|
+
'Both sides played by a computer with tactics of its own: footmen gang up on the weakest and fall back hurt, archers keep their distance, the knight hunts the archers. Sit back and watch, then lay them out again.',
|
|
683
|
+
tags: ['ai', 'computer player', 'brain', 'defineBrain', 'tactics', 'kiting', 'focus fire'],
|
|
684
|
+
map: () => exampleField({ name: 'Generals', size: 64, players: 2 }),
|
|
685
|
+
start: (world) => {
|
|
686
|
+
generalsArmies(world)
|
|
687
|
+
seatGenerals(world)
|
|
688
|
+
},
|
|
689
|
+
source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
id: 'showcase-rewind',
|
|
693
|
+
category: 'showcase',
|
|
694
|
+
title: 'Turn back time',
|
|
695
|
+
summary:
|
|
696
|
+
'A battle the world records twice a second. Let them fight, then turn back time: every unit stands where it stood, as hurt as it was, the fallen up again, and the fight goes on from there.',
|
|
697
|
+
tags: ['rewind', 'time', 'record', 'state', 'save', 'replay', 'component'],
|
|
698
|
+
map: () => exampleField({ name: 'Turn back time', players: 2 }),
|
|
699
|
+
start: (world) => rewindArmies(world),
|
|
700
|
+
source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
id: 'showcase-march',
|
|
704
|
+
category: 'showcase',
|
|
705
|
+
title: 'Companies on the march',
|
|
706
|
+
summary:
|
|
707
|
+
'A knight walks a road round the field, and seventy-two behind him follow in three companies, each in formation and facing the way they go. Change their shape as they march, or halt them.',
|
|
708
|
+
tags: ['formation', 'march', 'follow', 'leader', 'column', 'army', 'maneuver'],
|
|
709
|
+
map: () => exampleField({ name: 'March', size: 64 }),
|
|
710
|
+
start: (world) => column(world),
|
|
711
|
+
source: ['examples/showcase/index.ts', 'examples/showcase/looks.ts']
|
|
712
|
+
}
|
|
713
|
+
]
|
|
714
|
+
}
|