@v12-engine/create 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +26 -2
- package/base/_gitignore +10 -0
- package/base/index.html +20 -0
- package/base/package.json +23 -0
- package/base/src/ground.worker.ts +8 -0
- package/base/src/main.ts +61 -0
- package/base/tsconfig.json +14 -0
- package/base/vite.config.ts +25 -0
- package/bin.mjs +10 -0
- package/cli.mjs +381 -0
- package/index.mjs +286 -0
- package/layers/assets/package.json +5 -0
- package/layers/editor/package.json +8 -0
- package/layers/editor/src/editor.ts +13 -0
- package/layers/rooms/server.ts +69 -0
- package/layers/server/package.json +8 -0
- package/layers/server/server.ts +34 -0
- package/layers/source/package.json +12 -0
- package/layers/source/tsconfig.json +5 -0
- package/layers/source/vite.config.ts +45 -0
- package/layers/tests/package.json +8 -0
- package/package.json +46 -5
- package/starters/campaign/README.md +61 -0
- package/starters/campaign/files.txt +42 -0
- package/starters/campaign/layers/editor/src/editor.ts +18 -0
- package/starters/campaign/layers/rooms/server.ts +39 -0
- package/starters/campaign/layers/server/server.ts +33 -0
- package/starters/campaign/layers/tests/tests/rules.test.ts +320 -0
- package/starters/campaign/public/games/campaign/assets.json +1 -0
- package/starters/campaign/src/campaign/campaign.json +52 -0
- package/starters/campaign/src/campaign/carry.ts +47 -0
- package/starters/campaign/src/campaign/cinematics/intro.json +148 -0
- package/starters/campaign/src/campaign/cinematics/outro.json +92 -0
- package/starters/campaign/src/campaign/data.json +1 -0
- package/starters/campaign/src/campaign/fx.json +1 -0
- package/starters/campaign/src/campaign/game.ts +6 -0
- package/starters/campaign/src/campaign/ids.ts +77 -0
- package/starters/campaign/src/campaign/index.ts +121 -0
- package/starters/campaign/src/campaign/kills.ts +16 -0
- package/starters/campaign/src/campaign/looks.json +1 -0
- package/starters/campaign/src/campaign/maps.ts +34 -0
- package/starters/campaign/src/campaign/missions/homecoming.ts +160 -0
- package/starters/campaign/src/campaign/missions/siege.ts +91 -0
- package/starters/campaign/src/campaign/missions/warlord.ts +177 -0
- package/starters/campaign/src/campaign/prefabs/watchpost.prefab.json +18 -0
- package/starters/campaign/src/campaign/scenes.ts +46 -0
- package/starters/campaign/src/campaign/tiles.ts +8 -0
- package/starters/campaign/src/campaign/units.ts +390 -0
- package/starters/campaign/src/campaign/view/cinematics.ts +59 -0
- package/starters/campaign/src/campaign/view/fire.ts +115 -0
- package/starters/campaign/src/campaign/view/index.ts +334 -0
- package/starters/campaign/src/campaign/view/labels.ts +105 -0
- package/starters/campaign/src/campaign/view/rubble.ts +40 -0
- package/starters/campaign/src/campaign/view/waveLane.ts +88 -0
- package/starters/campaign/src/campaign/waves.json +36 -0
- package/starters/campaign/src/campaign/waves.ts +186 -0
- package/starters/campaign/src/campaign-editor.ts +43 -0
- package/starters/campaign/src/game.ts +2 -0
- package/starters/campaign/src/main.ts +70 -0
- package/starters/campaign/src/maps/fort.names.ts +31 -0
- package/starters/campaign/src/maps/fort.vgmap.json +1162 -0
- package/starters/campaign/src/maps/homestead.names.ts +36 -0
- package/starters/campaign/src/maps/homestead.vgmap.json +916 -0
- package/starters/campaign/src/maps/pass.names.ts +40 -0
- package/starters/campaign/src/maps/pass.vgmap.json +522 -0
- package/starters/campaign/src/maps.ts +2 -0
- package/starters/campaign/src/rules.ts +20 -0
- package/starters/campaign/src/waves-panel.ts +355 -0
- package/starters/empty/README.md +71 -0
- package/starters/empty/layers/tests/tests/rules.test.ts +31 -0
- package/starters/empty/src/game.ts +79 -0
- package/starters/empty/src/generate.worker.ts +9 -0
- package/starters/empty/src/maps.ts +44 -0
- package/starters/empty/src/rules.ts +67 -0
- package/starters/empty/src/tiles.ts +12 -0
- package/starters/melee/README.md +61 -0
- package/starters/melee/files.txt +60 -0
- package/starters/melee/layers/editor/src/editor.ts +13 -0
- package/starters/melee/layers/tests/tests/rules.test.ts +58 -0
- package/starters/melee/src/game.ts +2 -0
- package/starters/melee/src/maps/bay.names.ts +28 -0
- package/starters/melee/src/maps/bay.vgmap.json +3123 -0
- package/starters/melee/src/maps.ts +20 -0
- package/starters/melee/src/melee/abilities.ts +376 -0
- package/starters/melee/src/melee/bay.ts +319 -0
- package/starters/melee/src/melee/blight.ts +102 -0
- package/starters/melee/src/melee/cellLayers.json +11 -0
- package/starters/melee/src/melee/data.json +18 -0
- package/starters/melee/src/melee/floats.json +199 -0
- package/starters/melee/src/melee/formations.ts +46 -0
- package/starters/melee/src/melee/fx.json +1 -0
- package/starters/melee/src/melee/game.ts +6 -0
- package/starters/melee/src/melee/generator.ts +116 -0
- package/starters/melee/src/melee/heroSpells.ts +38 -0
- package/starters/melee/src/melee/hud.json +269 -0
- package/starters/melee/src/melee/index.ts +182 -0
- package/starters/melee/src/melee/items.ts +362 -0
- package/starters/melee/src/melee/languages/he.json +96 -0
- package/starters/melee/src/melee/looks.json +1 -0
- package/starters/melee/src/melee/maps.ts +53 -0
- package/starters/melee/src/melee/pings.json +16 -0
- package/starters/melee/src/melee/pings.ts +34 -0
- package/starters/melee/src/melee/quality.json +1 -0
- package/starters/melee/src/melee/saves.ts +12 -0
- package/starters/melee/src/melee/sounds.json +1 -0
- package/starters/melee/src/melee/undead.ts +683 -0
- package/starters/melee/src/melee/units.ts +785 -0
- package/starters/melee/src/melee/view/buildings.ts +33 -0
- package/starters/melee/src/melee/view/cheerLane.ts +42 -0
- package/starters/melee/src/melee/view/heroSpellLooks.ts +21 -0
- package/starters/melee/src/melee/view/index.ts +268 -0
- package/starters/melee/src/melee/view/languages.ts +10 -0
- package/starters/melee/src/melee/view/looks.ts +659 -0
- package/starters/melee/src/melee/view/undeadLooks.ts +220 -0
- package/starters/melee/src/melee/view/walls.ts +59 -0
- package/starters/melee/src/rules.ts +24 -0
- package/starters.mjs +35 -0
- package/v12.mjs +111 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "campaign.intro",
|
|
3
|
+
"name": "Intro",
|
|
4
|
+
"length": 22,
|
|
5
|
+
"lanes": [
|
|
6
|
+
{
|
|
7
|
+
"id": "camera",
|
|
8
|
+
"kind": "camera",
|
|
9
|
+
"items": [
|
|
10
|
+
{
|
|
11
|
+
"at": 0,
|
|
12
|
+
"value": {
|
|
13
|
+
"x": 20,
|
|
14
|
+
"z": 60,
|
|
15
|
+
"distance": 44,
|
|
16
|
+
"yaw": 0.4,
|
|
17
|
+
"pitch": 0.95
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"at": 6,
|
|
22
|
+
"value": {
|
|
23
|
+
"x": 22,
|
|
24
|
+
"z": 58,
|
|
25
|
+
"distance": 22,
|
|
26
|
+
"yaw": 0.9,
|
|
27
|
+
"pitch": 0.7
|
|
28
|
+
},
|
|
29
|
+
"path": "smooth",
|
|
30
|
+
"ease": "inOutCubic"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"at": 11,
|
|
34
|
+
"value": {
|
|
35
|
+
"x": 24,
|
|
36
|
+
"z": 56,
|
|
37
|
+
"distance": 26,
|
|
38
|
+
"yaw": 1.3,
|
|
39
|
+
"pitch": 0.8
|
|
40
|
+
},
|
|
41
|
+
"ease": "inOutSine"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"at": 12,
|
|
45
|
+
"value": {
|
|
46
|
+
"x": 58,
|
|
47
|
+
"z": 22,
|
|
48
|
+
"distance": 30,
|
|
49
|
+
"yaw": 2.4,
|
|
50
|
+
"pitch": 0.85
|
|
51
|
+
},
|
|
52
|
+
"transition": "dissolve"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"at": 17,
|
|
56
|
+
"value": {
|
|
57
|
+
"x": 62,
|
|
58
|
+
"z": 18,
|
|
59
|
+
"distance": 20,
|
|
60
|
+
"yaw": 2.9,
|
|
61
|
+
"pitch": 0.65
|
|
62
|
+
},
|
|
63
|
+
"path": "smooth",
|
|
64
|
+
"ease": "inOutCubic"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"at": 18,
|
|
68
|
+
"value": {
|
|
69
|
+
"x": 22,
|
|
70
|
+
"z": 58,
|
|
71
|
+
"distance": 32,
|
|
72
|
+
"yaw": 0,
|
|
73
|
+
"pitch": 0.95
|
|
74
|
+
},
|
|
75
|
+
"transition": "dissolve"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "screen",
|
|
81
|
+
"kind": "screen",
|
|
82
|
+
"items": [
|
|
83
|
+
{
|
|
84
|
+
"at": 0,
|
|
85
|
+
"for": 22,
|
|
86
|
+
"letterbox": 2.39,
|
|
87
|
+
"vignette": 0.3,
|
|
88
|
+
"in": 0.6,
|
|
89
|
+
"out": 0.8
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"at": 0,
|
|
93
|
+
"for": 2,
|
|
94
|
+
"fade": 1,
|
|
95
|
+
"in": 0,
|
|
96
|
+
"out": 2
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "dialog",
|
|
102
|
+
"kind": "dialog",
|
|
103
|
+
"items": [
|
|
104
|
+
{
|
|
105
|
+
"at": 2,
|
|
106
|
+
"for": 4,
|
|
107
|
+
"speaker": "campaign.aldric",
|
|
108
|
+
"text": "Home. Three years on the road, and the woods have gone dark while we were away."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"at": 6.5,
|
|
112
|
+
"for": 4.5,
|
|
113
|
+
"speaker": "campaign.brann",
|
|
114
|
+
"text": "The hall still stands, my lord. The farms, not all of them."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"at": 12.5,
|
|
118
|
+
"for": 4.5,
|
|
119
|
+
"speaker": "campaign.brann",
|
|
120
|
+
"text": "The Red Hand. Dug in beyond the trees, raiding by night."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"at": 17.5,
|
|
124
|
+
"for": 4,
|
|
125
|
+
"speaker": "campaign.aldric",
|
|
126
|
+
"text": "Then we burn them out."
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
"markers": [
|
|
132
|
+
{
|
|
133
|
+
"at": 0,
|
|
134
|
+
"name": "home",
|
|
135
|
+
"kind": "section"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"at": 12,
|
|
139
|
+
"name": "camp",
|
|
140
|
+
"kind": "section"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"at": 18,
|
|
144
|
+
"name": "back",
|
|
145
|
+
"kind": "section"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "campaign.outro",
|
|
3
|
+
"name": "Outro",
|
|
4
|
+
"length": 16,
|
|
5
|
+
"lanes": [
|
|
6
|
+
{
|
|
7
|
+
"id": "camera",
|
|
8
|
+
"kind": "camera",
|
|
9
|
+
"items": [
|
|
10
|
+
{
|
|
11
|
+
"at": 0,
|
|
12
|
+
"value": {
|
|
13
|
+
"x": 46,
|
|
14
|
+
"z": 20,
|
|
15
|
+
"distance": 18,
|
|
16
|
+
"yaw": 2.4,
|
|
17
|
+
"pitch": 0.6
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"at": 8,
|
|
22
|
+
"value": {
|
|
23
|
+
"x": 44,
|
|
24
|
+
"z": 22,
|
|
25
|
+
"distance": 36,
|
|
26
|
+
"yaw": 3.0,
|
|
27
|
+
"pitch": 0.8
|
|
28
|
+
},
|
|
29
|
+
"path": "smooth",
|
|
30
|
+
"ease": "inOutSine"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"at": 16,
|
|
34
|
+
"value": {
|
|
35
|
+
"x": 36,
|
|
36
|
+
"z": 30,
|
|
37
|
+
"distance": 64,
|
|
38
|
+
"yaw": 3.6,
|
|
39
|
+
"pitch": 0.9
|
|
40
|
+
},
|
|
41
|
+
"ease": "inOutSine"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "screen",
|
|
47
|
+
"kind": "screen",
|
|
48
|
+
"items": [
|
|
49
|
+
{
|
|
50
|
+
"at": 0,
|
|
51
|
+
"for": 16,
|
|
52
|
+
"letterbox": 2.39,
|
|
53
|
+
"vignette": 0.35,
|
|
54
|
+
"in": 0.6,
|
|
55
|
+
"out": 0
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"at": 13,
|
|
59
|
+
"for": 3,
|
|
60
|
+
"fade": 1,
|
|
61
|
+
"in": 3,
|
|
62
|
+
"out": 0
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "dialog",
|
|
68
|
+
"kind": "dialog",
|
|
69
|
+
"items": [
|
|
70
|
+
{
|
|
71
|
+
"at": 1,
|
|
72
|
+
"for": 4,
|
|
73
|
+
"speaker": "campaign.brann",
|
|
74
|
+
"text": "It is done, my lord. The Red Hand is finished."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"at": 6,
|
|
78
|
+
"for": 5,
|
|
79
|
+
"speaker": "campaign.aldric",
|
|
80
|
+
"text": "Take the prisoners home. Then we rebuild the farms. The long road ends where it began."
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"markers": [
|
|
86
|
+
{
|
|
87
|
+
"at": 13,
|
|
88
|
+
"name": "fade",
|
|
89
|
+
"kind": "section"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { defineGame } from '@v12-engine/client'
|
|
2
|
+
import { campaign } from './index'
|
|
3
|
+
import { campaignView } from './view'
|
|
4
|
+
|
|
5
|
+
/** The campaign's game, whole: its rules and how it looks. What a page launches, and its editor makes maps for */
|
|
6
|
+
export const campaignGame = defineGame({ rules: campaign, view: campaignView })
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineIds, type IdOf, type PlayerId } from '@v12-engine/engine'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The ids of the campaign, named once (`defineIds`): the compiler checks them wherever they are written, in the rules
|
|
5
|
+
* and in the view (`GameView<CampaignUnit, CampaignResource, CampaignMessage>`), where plain text would be found wrong
|
|
6
|
+
* only when the game runs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Its unit types: the player's, the Red Hand's, the creeps', and what the maps are dressed with */
|
|
10
|
+
export const UNIT = defineIds(
|
|
11
|
+
'peasant',
|
|
12
|
+
'footman',
|
|
13
|
+
'archer',
|
|
14
|
+
'knight',
|
|
15
|
+
'hall',
|
|
16
|
+
'farm',
|
|
17
|
+
'barracks',
|
|
18
|
+
'tower',
|
|
19
|
+
'wall',
|
|
20
|
+
'trader',
|
|
21
|
+
'raider',
|
|
22
|
+
'warlock',
|
|
23
|
+
'brute',
|
|
24
|
+
'warlord',
|
|
25
|
+
'gnoll',
|
|
26
|
+
'prisoner',
|
|
27
|
+
'tree',
|
|
28
|
+
'goldmine'
|
|
29
|
+
)
|
|
30
|
+
export type CampaignUnit = IdOf<typeof UNIT>
|
|
31
|
+
|
|
32
|
+
/** What its players have amounts of */
|
|
33
|
+
export const RESOURCE = defineIds('gold')
|
|
34
|
+
export type CampaignResource = IdOf<typeof RESOURCE>
|
|
35
|
+
|
|
36
|
+
/** The keys of what it says: the rules say the key, the view has the words (`GameView.hud.messages`) */
|
|
37
|
+
export const MSG = defineIds(
|
|
38
|
+
'objectiveRaze',
|
|
39
|
+
'objectiveHall',
|
|
40
|
+
'objectiveCaravan',
|
|
41
|
+
'objectiveHold',
|
|
42
|
+
'objectiveTowers',
|
|
43
|
+
'objectiveWarlord',
|
|
44
|
+
'objectiveHero',
|
|
45
|
+
'objectivePrisoners',
|
|
46
|
+
'caravanFound',
|
|
47
|
+
'waveComing',
|
|
48
|
+
'waveBeaten',
|
|
49
|
+
'gateShut',
|
|
50
|
+
'gateOpen',
|
|
51
|
+
'checkpointWave',
|
|
52
|
+
'heroFell',
|
|
53
|
+
'hallFell',
|
|
54
|
+
'homeStart',
|
|
55
|
+
'homeCamp',
|
|
56
|
+
'homeFord',
|
|
57
|
+
'homeDone',
|
|
58
|
+
'siegeStart',
|
|
59
|
+
'siegeGate',
|
|
60
|
+
'siegeHalfway',
|
|
61
|
+
'siegeLast',
|
|
62
|
+
'passStart',
|
|
63
|
+
'passShop',
|
|
64
|
+
'passPrisoners',
|
|
65
|
+
'passDuel',
|
|
66
|
+
'passCharge'
|
|
67
|
+
)
|
|
68
|
+
export type CampaignMessage = IdOf<typeof MSG>
|
|
69
|
+
|
|
70
|
+
/** Who is who: the player at the screen, and the Red Hand */
|
|
71
|
+
export const PLAYER: PlayerId = 1
|
|
72
|
+
export const ENEMY: PlayerId = 2
|
|
73
|
+
|
|
74
|
+
/** Who speaks: their names and looks are the view's (`GameView.cinematics.speakers`) */
|
|
75
|
+
export const ALDRIC = 'campaign.aldric'
|
|
76
|
+
export const BRANN = 'campaign.brann'
|
|
77
|
+
export const VARGA = 'campaign.varga'
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type GameMap, type GameModule, hotRules, objectiveVictory, prefabsFrom, withData, withHeroes, type World } from '@v12-engine/engine'
|
|
2
|
+
import { ENEMY, PLAYER } from './ids'
|
|
3
|
+
import { mapKeyOf, maps } from './maps'
|
|
4
|
+
import { homecoming, homecomingListeners, homecomingSequences, outpost, redHand, reinforcements } from './missions/homecoming'
|
|
5
|
+
import { siege, siegeListeners, siegeSequences } from './missions/siege'
|
|
6
|
+
import { parley, vargasMen, warlord, warlordListeners, warlordSequences } from './missions/warlord'
|
|
7
|
+
import { countKills, kills } from './kills'
|
|
8
|
+
import { tiles } from './tiles'
|
|
9
|
+
import { abilities, buffs, effects, items, units } from './units'
|
|
10
|
+
import { nextWave, reportWaves, siegeWaves, waveBeaten, waveComing, waveListeners, waves, wavesFile, waveTimers } from './waves'
|
|
11
|
+
import dataFile from './data.json'
|
|
12
|
+
import watchpost from './prefabs/watchpost.prefab.json'
|
|
13
|
+
|
|
14
|
+
export { campaignMap, DEFAULT_MAP, maps } from './maps'
|
|
15
|
+
export { ENEMY, MSG, PLAYER, UNIT, type CampaignMessage, type CampaignResource, type CampaignUnit } from './ids'
|
|
16
|
+
export { homecoming, siege, warlord }
|
|
17
|
+
export { kills } from './kills'
|
|
18
|
+
export { homecomingCarry, type RoadData, siegeCarriedIn, warlordCarry } from './carry'
|
|
19
|
+
export { builtWaves, readWaves, siegeWaves, type Wave, WAVE_UNITS, wavesFile, wavesOf } from './waves'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The campaign's game: a game module of its own on the engine's public API, and nothing else. Its missions are won
|
|
23
|
+
* and lost by their objectives (`objectiveVictory`), not by who has buildings left; each mission's rules are in
|
|
24
|
+
* `missions/`, and the maps say which mission they are (`startMission`)
|
|
25
|
+
*
|
|
26
|
+
* What it brings, and what of the engine it uses for that:
|
|
27
|
+
* - its units, buildings, hero, abilities, items (data: `units.ts`), and what the editor's Data panel may change of them
|
|
28
|
+
* - a resource, gold, dug at mines and paid as bounty
|
|
29
|
+
* - the waves of the siege (`waves.ts`): timers, a component in the world, an event, a command to call one in early
|
|
30
|
+
* - how many each tower has killed (`kills.ts`): a component of an entity every player is sent, which its view pins
|
|
31
|
+
* over the tower
|
|
32
|
+
* - missions: objectives, sequences of dialog and cinematics, areas of the map the rules watch, timers, prefabs, and
|
|
33
|
+
* computer players that are mission brains (`@v12-engine/engine/ai`), each listed so that a save finds it by its id
|
|
34
|
+
*/
|
|
35
|
+
export const campaign: GameModule = withData(
|
|
36
|
+
{
|
|
37
|
+
// the maps of this game say they are its own by this id: change it, and change theirs (`"game"`) with it
|
|
38
|
+
id: 'my-game',
|
|
39
|
+
name: 'My game',
|
|
40
|
+
units,
|
|
41
|
+
abilities,
|
|
42
|
+
buffs,
|
|
43
|
+
items,
|
|
44
|
+
effects,
|
|
45
|
+
resources: [{ id: 'gold', name: 'Gold', start: 300 }],
|
|
46
|
+
currency: 'gold',
|
|
47
|
+
// its hero, who earns levels and is brought back at an altar, and the creeps' camps (`withHeroes`)
|
|
48
|
+
packs: [withHeroes()],
|
|
49
|
+
tiles,
|
|
50
|
+
// the maps it ships: a save of a game on one keeps only what changed of its ground
|
|
51
|
+
maps: Object.values(maps),
|
|
52
|
+
// the outpost in code, and the designers' prefabs (`prefabs/*.prefab.json`), as the rules may spawn them too
|
|
53
|
+
prefabs: [outpost, ...prefabsFrom({ 'watchpost.prefab.json': watchpost }, { prefix: 'campaign.' })],
|
|
54
|
+
brains: [redHand, vargasMen],
|
|
55
|
+
commands: [nextWave],
|
|
56
|
+
listeners: [...waveListeners, countKills, ...homecomingListeners, ...siegeListeners, ...warlordListeners],
|
|
57
|
+
timers: [...waveTimers, reinforcements],
|
|
58
|
+
files: [wavesFile],
|
|
59
|
+
sequences: [...homecomingSequences, ...siegeSequences, ...warlordSequences],
|
|
60
|
+
// what it keeps of its own: the waves' state, each tower's kills (sent to everyone, pinned over it by the view), and
|
|
61
|
+
// the answer at Varga's gate
|
|
62
|
+
components: [waves, kills, parley],
|
|
63
|
+
events: [waveBeaten, waveComing],
|
|
64
|
+
editor: {
|
|
65
|
+
// the player and the Red Hand, always: the editor sets where they start, not who they are
|
|
66
|
+
players: [
|
|
67
|
+
{ id: PLAYER, name: 'Aldric', team: 1, color: [0.2, 0.45, 0.95] },
|
|
68
|
+
{ id: ENEMY, name: 'The Red Hand', team: 2, color: [0.9, 0.2, 0.18], controller: 'computer' }
|
|
69
|
+
],
|
|
70
|
+
playersNote: 'Aldric is you. The Red Hand is the computer, as each mission says.',
|
|
71
|
+
// where the game names a unit type beyond the map: the waves that send it (Used by, Find references)
|
|
72
|
+
references: (ref, _map, registry) =>
|
|
73
|
+
'type' in ref
|
|
74
|
+
? siegeWaves({ registry }).flatMap((wave, i) =>
|
|
75
|
+
wave.units.some((unit) => unit.type === ref.type) ? [{ what: `wave ${i + 1}`, where: 'waves.json' }] : []
|
|
76
|
+
)
|
|
77
|
+
: []
|
|
78
|
+
},
|
|
79
|
+
victory: objectiveVictory,
|
|
80
|
+
report: (world) => reportWaves(world),
|
|
81
|
+
// a save made before a patch is loaded after it (`saves.migrate` takes the step)
|
|
82
|
+
saves: { version: 1 },
|
|
83
|
+
// what the editor's Data panel may change, kept in `data.json`: numbers and prices, never what a unit is
|
|
84
|
+
editable: {
|
|
85
|
+
units: (def) =>
|
|
86
|
+
def.kind === 'building'
|
|
87
|
+
? ['name', { path: 'stats.health', min: 1 }, 'stats.armor', { path: 'cost.gold', kind: 'integer', min: 0 }, 'buildTime']
|
|
88
|
+
: def.kind === 'unit'
|
|
89
|
+
? [
|
|
90
|
+
'name',
|
|
91
|
+
{ path: 'stats.health', min: 1 },
|
|
92
|
+
'stats.attackDamage',
|
|
93
|
+
'stats.attackSpeed',
|
|
94
|
+
'stats.armor',
|
|
95
|
+
'stats.moveSpeed',
|
|
96
|
+
{ path: 'cost.gold', kind: 'integer', min: 0 }
|
|
97
|
+
]
|
|
98
|
+
: null,
|
|
99
|
+
abilities: [{ path: 'cooldown', min: 0 }, { path: 'manaCost', min: 0 }, 'description'],
|
|
100
|
+
items: [{ path: 'cost.gold', kind: 'integer', min: 0 }, 'description']
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
dataFile
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
/** Each map's mission, by the map's file: what `startMission` gives a world made from it */
|
|
107
|
+
const MISSIONS: Record<string, (world: World, fresh: boolean) => void> = { homestead: homecoming, fort: siege, pass: warlord }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gives a world the mission of the map it was made from: the campaign's scene does it with `fresh` as it knows it;
|
|
111
|
+
* the editor's Play here, a server and the tests do it through `start` with a fresh world. A map of none (one made in
|
|
112
|
+
* the editor for trying things) gets no mission: the game plays on, won by nobody
|
|
113
|
+
*/
|
|
114
|
+
export function startMission(world: World, map: Pick<GameMap, 'name'> | undefined, fresh = true): void {
|
|
115
|
+
const key = map && mapKeyOf(map)
|
|
116
|
+
const mission = key && MISSIONS[key]
|
|
117
|
+
if (mission) mission(world, fresh)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// saved while it runs in the dev server: the games and editors of the page take the new version (hot reload)
|
|
121
|
+
if (import.meta.hot) import.meta.hot.accept((module) => hotRules(import.meta.hot, campaign, module?.campaign))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { entityDied } from '@v12-engine/engine'
|
|
2
|
+
import { defineComponent, defineListener } from '@v12-engine/engine'
|
|
3
|
+
import { ENEMY, PLAYER } from './ids'
|
|
4
|
+
|
|
5
|
+
/** How many of the Red Hand a tower of the fort has finished off. Everyone sees it: the view puts it over the tower */
|
|
6
|
+
export const kills = defineComponent<number>('campaign.kills', { replicate: 'everyone' })
|
|
7
|
+
|
|
8
|
+
/** One of the Red Hand that dies counts for the building of the player's that killed it */
|
|
9
|
+
export const countKills = defineListener({
|
|
10
|
+
event: entityDied,
|
|
11
|
+
id: 'campaign.countKills',
|
|
12
|
+
run(_world, { entity, killer }) {
|
|
13
|
+
if (entity.owner !== ENEMY || !killer || killer.owner !== PLAYER || killer.def.kind !== 'building') return
|
|
14
|
+
killer.set(kills, (killer.get(kills) ?? 0) + 1)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type GameMap, mapsFrom } from '@v12-engine/engine'
|
|
2
|
+
|
|
3
|
+
import homestead from '../maps/homestead.vgmap.json'
|
|
4
|
+
import fort from '../maps/fort.vgmap.json'
|
|
5
|
+
import pass from '../maps/pass.vgmap.json'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The maps of the campaign, one a mission: the files in `src/maps`, made in the editor (`/?editor`, whose Save writes
|
|
9
|
+
* them there, with a names file each). A new map is a new file there, imported here as these are. (Not with Vite's
|
|
10
|
+
* `import.meta.glob`: a server run by Node has no such thing.)
|
|
11
|
+
*/
|
|
12
|
+
const files = {
|
|
13
|
+
'./maps/homestead.vgmap.json': homestead,
|
|
14
|
+
'./maps/fort.vgmap.json': fort,
|
|
15
|
+
'./maps/pass.vgmap.json': pass
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The maps by the name of their file, without `.vgmap.json`: `homestead` */
|
|
19
|
+
export const maps: Readonly<Record<string, GameMap>> = mapsFrom(files)
|
|
20
|
+
|
|
21
|
+
/** The map a game is on when nothing says which, and the one the editor opens first */
|
|
22
|
+
export const DEFAULT_MAP = 'homestead'
|
|
23
|
+
|
|
24
|
+
/** A map by name, as a copy: a game changes nothing of what the next one starts from */
|
|
25
|
+
export function campaignMap(name = DEFAULT_MAP): GameMap {
|
|
26
|
+
const map = maps[name]
|
|
27
|
+
if (!map) throw new Error(`The campaign has no map "${name}": its maps are ${Object.keys(maps).join(', ')}`)
|
|
28
|
+
return structuredClone(map)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The file a map came from, by its name as the map says it: how the rules know which mission a map is */
|
|
32
|
+
export function mapKeyOf(map: Pick<GameMap, 'name'>): string | undefined {
|
|
33
|
+
return Object.entries(maps).find(([, each]) => each.name === map.name)?.[0]
|
|
34
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { enteredArea, entityDied } from '@v12-engine/engine'
|
|
2
|
+
import {
|
|
3
|
+
areaShapeOf,
|
|
4
|
+
cinematic,
|
|
5
|
+
defineListener,
|
|
6
|
+
defineObjective,
|
|
7
|
+
definePrefab,
|
|
8
|
+
defineSequence,
|
|
9
|
+
defineTimer,
|
|
10
|
+
earn,
|
|
11
|
+
talk,
|
|
12
|
+
wait,
|
|
13
|
+
waitFor,
|
|
14
|
+
type World
|
|
15
|
+
} from '@v12-engine/engine'
|
|
16
|
+
import { holdPosition, missionBrain, waveFrom } from '@v12-engine/engine/ai'
|
|
17
|
+
import { ALDRIC, BRANN, ENEMY, MSG, PLAYER, UNIT } from '../ids'
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Mission one, Homecoming: Aldric comes home to a base with the Red Hand dug in beyond the woods. Burn their camp,
|
|
21
|
+
* keep the hall. On the way: a ford where Brann has a word, a lost caravan worth finding, an outpost the rules put
|
|
22
|
+
* up from a prefab, and a computer that holds its camp and sends what reaches it at the hall
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const razeCamp = defineObjective({ id: 'campaign.raze', text: MSG.objectiveRaze })
|
|
26
|
+
export const keepHall = defineObjective({ id: 'campaign.hall', text: MSG.objectiveHall })
|
|
27
|
+
export const findCaravan = defineObjective({ id: 'campaign.caravan', text: MSG.objectiveCaravan, required: false })
|
|
28
|
+
|
|
29
|
+
/** The areas of the map the rules watch, by the names of its regions: added as the mission starts (a save keeps them) */
|
|
30
|
+
const FORD = 'campaign.ford'
|
|
31
|
+
const CARAVAN = 'campaign.caravan'
|
|
32
|
+
const REGIONS: [string, string][] = [
|
|
33
|
+
[FORD, 'ford'],
|
|
34
|
+
[CARAVAN, 'caravan']
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/** Gold in the caravan's wreck */
|
|
38
|
+
const CARAVAN_GOLD = 200
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* An outpost: a tower between two walls and an archer behind them. A prefab in code, spawned where the map says
|
|
42
|
+
* (`outpost`); the designers' prefabs are files beside it (`prefabs/`), placed in the editor
|
|
43
|
+
*/
|
|
44
|
+
export const outpost = definePrefab({
|
|
45
|
+
id: 'campaign.outpost',
|
|
46
|
+
name: 'Outpost',
|
|
47
|
+
contents: {
|
|
48
|
+
objects: [
|
|
49
|
+
{ type: UNIT.tower, x: 0, z: 0 },
|
|
50
|
+
{ type: UNIT.wall, x: -2.5, z: 0.5 },
|
|
51
|
+
{ type: UNIT.wall, x: 2.5, z: 0.5 },
|
|
52
|
+
{ type: UNIT.archer, x: 0, z: -1.8, facing: Math.PI }
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Red Hand of the camp: it holds the camp with what it has, and every so often sends what the reinforcements
|
|
59
|
+
* bring (`waveFrom`: what the mission spawns at the camp goes for the hall in threes). A mission brain: parts and
|
|
60
|
+
* nothing else, no building, no training (`@v12-engine/engine/ai`)
|
|
61
|
+
*/
|
|
62
|
+
export const redHand = missionBrain({
|
|
63
|
+
id: 'campaign.redHand',
|
|
64
|
+
modules: [holdPosition('camp'), waveFrom('camp', { group: 3, every: 90 })]
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/** Every so often, two raiders more at the camp: the brain sends them */
|
|
68
|
+
export const reinforcements = defineTimer('campaign.reinforcements', (world) => {
|
|
69
|
+
if (world.winner !== null) return
|
|
70
|
+
const camp = world.map.point('camp')
|
|
71
|
+
if (!enemyBuildings(world).length) return
|
|
72
|
+
for (const dx of [-1.5, 1.5]) world.spawn(UNIT.raider, ENEMY, { x: camp.x + dx, z: camp.z + 1 })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/** The Red Hand's buildings, standing */
|
|
76
|
+
export const enemyBuildings = (world: World) => [...world.index.ownedBy(ENEMY)].filter((entity) => entity.alive && entity.def.kind === 'building')
|
|
77
|
+
|
|
78
|
+
/** The intro, then Brann's briefing: a sequence, skippable a line at a time, saved halfway like everything else */
|
|
79
|
+
export const briefing = defineSequence('campaign.briefing', [
|
|
80
|
+
// the screen lets the first tick pass before it draws: the intro is the first thing seen, the game waiting
|
|
81
|
+
wait(0),
|
|
82
|
+
cinematic('campaign.intro', { hold: true, wait: 'ended' }),
|
|
83
|
+
wait(0.5),
|
|
84
|
+
...talk(PLAYER, [
|
|
85
|
+
{ speaker: BRANN, text: MSG.homeStart, seconds: 5 },
|
|
86
|
+
{ speaker: ALDRIC, text: MSG.homeCamp, seconds: 5 }
|
|
87
|
+
])
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
/** At the ford, a word, and the camp shown for a moment: whoever of the player's walks in first */
|
|
91
|
+
export const atTheFord = defineSequence('campaign.atTheFord', [
|
|
92
|
+
waitFor(enteredArea, (_world, { area, entity }) => area === FORD && entity.owner === PLAYER),
|
|
93
|
+
(world) => {
|
|
94
|
+
const camp = world.map.point('camp')
|
|
95
|
+
world.reveal(world.player(PLAYER).team, camp, 12, 10)
|
|
96
|
+
},
|
|
97
|
+
...talk(PLAYER, [{ speaker: BRANN, text: MSG.homeFord, seconds: 5 }])
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
/** The caravan found: its gold, and the optional objective done */
|
|
101
|
+
export const caravanFound = defineListener({
|
|
102
|
+
event: enteredArea,
|
|
103
|
+
id: 'campaign.caravanFound',
|
|
104
|
+
run(world, { area, entity }) {
|
|
105
|
+
if (area !== CARAVAN || entity.owner !== PLAYER || !world.objectives.has(PLAYER, findCaravan.id)) return
|
|
106
|
+
if (world.objectives.get(PLAYER, findCaravan.id).status !== 'open') return
|
|
107
|
+
earn(world.player(PLAYER).resources, 'gold', CARAVAN_GOLD)
|
|
108
|
+
world.objectives.complete(PLAYER, findCaravan.id)
|
|
109
|
+
world.message(PLAYER, { key: MSG.caravanFound, params: { gold: CARAVAN_GOLD } }, 'info')
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/** A building fell: the camp's, one less to raze, the last of them the mission won; the hall, the mission lost */
|
|
114
|
+
export const buildingFell = defineListener({
|
|
115
|
+
event: entityDied,
|
|
116
|
+
id: 'campaign.homeBuildingFell',
|
|
117
|
+
run(world, { entity }) {
|
|
118
|
+
if (entity.def.kind !== 'building' || !world.objectives.has(PLAYER, razeCamp.id)) return
|
|
119
|
+
const { objectives } = world
|
|
120
|
+
if (entity.owner === ENEMY) {
|
|
121
|
+
const left = enemyBuildings(world).length
|
|
122
|
+
const total = objectives.get(PLAYER, razeCamp.id).progress?.max ?? left
|
|
123
|
+
objectives.progress(PLAYER, razeCamp.id, total - left)
|
|
124
|
+
if (left) return
|
|
125
|
+
objectives.complete(PLAYER, razeCamp.id)
|
|
126
|
+
objectives.complete(PLAYER, keepHall.id)
|
|
127
|
+
if (objectives.get(PLAYER, findCaravan.id).status === 'open') objectives.fail(PLAYER, findCaravan.id)
|
|
128
|
+
world.sequences.start(wellDone)
|
|
129
|
+
} else if (entity.owner === PLAYER && entity.def.id === UNIT.hall) objectives.fail(PLAYER, keepHall.id, MSG.hallFell)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const wellDone = defineSequence('campaign.homeDone', talk(PLAYER, [{ speaker: ALDRIC, text: MSG.homeDone, seconds: 4 }]))
|
|
134
|
+
|
|
135
|
+
export const homecomingSequences = [briefing, atTheFord, wellDone]
|
|
136
|
+
export const homecomingListeners = [caravanFound, buildingFell]
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Gives the mission: once, as the world is made (`fresh`). A game loaded from a save has all of it already: the
|
|
140
|
+
* objectives, the areas, the sequences halfway, the computer's seat and its memory, the timers
|
|
141
|
+
*/
|
|
142
|
+
export function homecoming(world: World, fresh: boolean): void {
|
|
143
|
+
if (!fresh) return
|
|
144
|
+
const { objectives } = world
|
|
145
|
+
objectives.add(PLAYER, razeCamp, { progress: { value: 0, max: enemyBuildings(world).length } })
|
|
146
|
+
objectives.add(PLAYER, keepHall)
|
|
147
|
+
objectives.add(PLAYER, findCaravan)
|
|
148
|
+
for (const [id, region] of REGIONS) {
|
|
149
|
+
const shape = areaShapeOf({ region }, world.map.regions)
|
|
150
|
+
if (shape) world.areas.add({ id, shape })
|
|
151
|
+
}
|
|
152
|
+
// the outpost, where the map says: a prefab spawned by the rules
|
|
153
|
+
world.spawnPrefab(outpost, { at: world.map.point('outpost'), owner: PLAYER })
|
|
154
|
+
// the Red Hand's computer, remembered with the world: a save seats it again as it loads
|
|
155
|
+
const hall = world.named('hall')
|
|
156
|
+
world.computers.seat(ENEMY, { enemyBases: [hall ? { x: hall.pos.x, z: hall.pos.z } : world.map.point('outpost')], brain: redHand })
|
|
157
|
+
world.timers.every(90, reinforcements, {})
|
|
158
|
+
world.sequences.start(briefing)
|
|
159
|
+
world.sequences.start(atTheFord)
|
|
160
|
+
}
|