@netforge/template-party 0.0.0-stage → 0.2.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/llms.txt ADDED
@@ -0,0 +1,375 @@
1
+ # Net Forge party co-op template (ROMPHOUSE)
2
+
3
+ > A party game for 2 to 8 friends in one big playroom: a show of five
4
+ > quick rounds, each a different minigame with one goal for the whole
5
+ > team. Win three and the show is a hit. You change the game by editing
6
+ > src/game.ts only. The template already has the playroom (floor and
7
+ > walls), weapons that fire by themselves, XP stars, level-ups, the boss
8
+ > machinery, co-op revive, netcode, rendering, touch controls, earn and the
9
+ > store. Do not write renderers, netcode or physics.
10
+
11
+ ## Files
12
+
13
+ - src/game.ts: THE GAME. Declarations against @netforge/engine:
14
+ game(), prefab(), weapon(), upgrade(), system(), earn.rule(),
15
+ earn.stake(), store.sku(), keeper(), achievement(), mission(), title().
16
+ - packages/engine/GAME-DESIGN.md: the design rules the playtest gate scores
17
+ (a readable opening, off-screen spawns, i-frames, telegraphs, fast levels).
18
+ - client/names.ts: display names for weapon, upgrade and earn ids (labels).
19
+ - Models come from the asset library (real CC0 models; the full list of ids is in your instructions). client/library.ts holds the fallback shapes drawn until a model loads.
20
+ - Everything else is template machinery.
21
+
22
+ ## Rules the build enforces
23
+
24
+ - Asset ids look like "style.lowpoly.slime_pink", never URLs or paths, and must be in the asset library.
25
+ - game({ style }) is the art style the creator picked: lowpoly, toon, ps1, pixel, voxel or handdrawn. Keep it. The template draws everything in it, and a "style.lowpoly.<name>" id draws as that style's own model of the same name when the style has one.
26
+ - game({ template: "party" }) and one room in game({ field }): a floor and four walls.
27
+ - Ids are lower_snake_case.
28
+ - Every game declares at least one earn.rule (mandatory earn) and one earn.stake (the buy-in run).
29
+ - A party game needs one enemy prefab, one weapon and players.weapon (or keepers).
30
+ - Drops must be pickup prefabs. At most 15 weapons (evolutions count) and 8 upgrades.
31
+ - Run `pnpm --filter @netforge/template-party test` (playtest bots).
32
+
33
+ ## Units
34
+
35
+ Seconds, metres, metres per second, hit points. Tick rate 30 Hz. Players
36
+ run at 7 m/s; keep chasers at 3-4 m/s and fast ones under 6.
37
+
38
+ ## The playroom (game({ field }))
39
+
40
+ const ROOM = 30;
41
+ field: {
42
+ halfSize: ROOM + 2,
43
+ pillars: [[12, 12, 1.6], ...], // party tables: [x, z, radius]
44
+ floors: [[0, 0, ROOM, ROOM]], // the floor: [x, z, halfX, halfZ]
45
+ walls: [[0, ROOM + 1, ROOM + 2, 1], ...], // the four walls: [x, z, halfX, halfZ]
46
+ }
47
+ The room is bigger than the screen (60 m across against about 40 m in
48
+ view), so spawnEdge can always bring a crowd in just off screen. Players
49
+ start at [0, 0]. Walls and tables stop players, enemies and bolts.
50
+
51
+ ## The show (the "show" system in game.ts)
52
+
53
+ The show is a system you can read and change:
54
+
55
+ - ROUNDS is a list of minigames: { name, seconds, start(), won(), lastsOut }.
56
+ start() runs as the round opens (spawn its crowd, scatter its props,
57
+ start a trickle with every(...)). won() is checked every half second, and
58
+ true ends the round early as won. At the bell the round is won if won()
59
+ holds, or, for lastsOut rounds, if nobody went down.
60
+ - The first four rounds are shuffled on the sim's random each show; the
61
+ last (the Big Balloon) is always the finale.
62
+ - Every won round emits round.won (earn rules count it); every round's end
63
+ emits wave.cleared (the HUD's ROUND n); a clean sweep emits show.perfect.
64
+ Between rounds (BREATHER seconds) the leftovers are cleared and anyone
65
+ who is out comes back.
66
+ - HIT_AT rounds won make the show a hit (end("won")); fewer, it ends
67
+ "lost". Either way the end screen shows and the next show starts.
68
+ - Something must happen every few seconds in every round: a round with 30
69
+ quiet seconds fails the playtest (softlock). Give each round a trickle.
70
+
71
+ ### Add a minigame
72
+ {
73
+ name: "DODGEBALL",
74
+ seconds: 35,
75
+ lastsOut: true, // won if nobody goes down
76
+ start: () => {
77
+ spawnEdge("spritzer", { count: 6 + party() });
78
+ every(4, () => { if (current === 5) spawnEdge("spritzer", { count: 2 }); });
79
+ },
80
+ won: () => false,
81
+ },
82
+ Append it to ROUNDS (its index is its `current` value) and include it in the
83
+ shuffle (the shuffle leaves the last entry, the finale, where it is).
84
+ Counters you can use: kills (enemy kills this round), downs (players down
85
+ this round), alive("prefab"), and on("<prefab>.killed") or
86
+ on("miniboss.killed") for a flag.
87
+
88
+ ## Going down
89
+
90
+ players: { downed: { seconds: 8, crawl: 0.5, selfRevives: 2, respawn: 3 } }
91
+ A party forgives: a short down, a crawl, two free get-ups a show (Space),
92
+ then back in 3 s beside a friend. Everyone out returns between rounds.
93
+
94
+ ## Common changes
95
+
96
+ ### Make it harder or easier
97
+ Change a round's goal (poppers(), the scatter count), its seconds, the
98
+ trickle sizes, or HIT_AT. Easier also: players.hitIframes (0.8 s),
99
+ players.magnet, more selfRevives.
100
+
101
+ ### A longer or shorter show
102
+ Add or remove ROUNDS entries (keep 3 to 8). ROUND_SECONDS sets the default
103
+ length; each round may set its own.
104
+
105
+ ### Add an enemy
106
+ prefab("jester", { model: "style.lowpoly.blob_wizard", health: 30, speed: 3.2, radius: 0.4, damage: 6, ai: "chase", drops: ["star_small"] });
107
+ then bring it into a round's start() with spawnEdge("jester", { count: 8 }),
108
+ and add its id to the sweep list so it is cleared between rounds.
109
+ ai is the enemy's role. Every big hit is telegraphed on the ground first:
110
+ - "chase" (default), "tank" (a chaser; give it mass), "idle"/"none" (stands still)
111
+ - "runner": aims where you will be. Fast, fragile.
112
+ - "charger": stops inside 6 m, shows a lane 0.55 s, then charges along it.
113
+ - "ranged": keeps its distance, shows an aim line, fires a slow dodgeable shot
114
+ (only with a clear line: walls and tables are cover).
115
+ - "bomber": walks up, arms a 1 s fuse circle, explodes (hurts enemies too).
116
+ - "shielder" / "healer": every few seconds shields or heals nearby allies.
117
+ - "summoner": keeps away and calls adds: attack: { summon: "fizz", count: 2 }.
118
+ - "flee" (a carrier to catch, like the party pup), "wander".
119
+
120
+ ### Add elites
121
+ An elite is x6 health, x1.5 damage, x5 XP and a gold rim, and drops a chest:
122
+ spawnEdge("wobbler", { count: 1, elite: true }) in a round's start().
123
+
124
+ ### Add a weapon
125
+ weapon("frost_ring", {
126
+ kind: "nova", damage: 6, cooldown: 0.8, range: 5, knockback: 8,
127
+ levels: [{ damage: "+30%" }, { range: "+15%" }, { cooldown: "-15%" }, { damage: "+30%" }, { knockback: 3 }],
128
+ });
129
+ kinds:
130
+ - "nova": a burst in range; "aura": a field that burns what is inside, every cooldown
131
+ - "bolt": count projectiles at the nearest enemy (speed, pierce)
132
+ - "orbit": count blades at range metres, speed rad/s
133
+ - "cone": a sweep of spread degrees where the keeper faces (360: all around)
134
+ - "chain": strikes the nearest, then jumps count times, each within speed m
135
+ - "meteor": count circles of blast m land on enemies in range, 0.6 s after showing
136
+ - "boomerang": flies out range m and back, hitting everything both ways
137
+ levels: what each level changes from the one before (5-8 levels). A number
138
+ adds (count: 1, pierce: 2, cooldown: -0.1 s), a percent scales ("+40%").
139
+ Cards show these changes ("+1 projectile, -20% cooldown"). It is offered at
140
+ level-up automatically. Add a label in client/names.ts.
141
+
142
+ ### Add an evolution
143
+ weapon("frost_ring", { ..., evolve: { into: "blizzard", requires: "iron_skin" } });
144
+ weapon("blizzard", { kind: "nova", damage: 20, cooldown: 0.6, range: 9, knockback: 12 });
145
+ At max level, with the passive "iron_skin" taken, the "blizzard" card always
146
+ shows; it replaces frost_ring in its slot. Evolutions are never offered on
147
+ their own. Give them about x1.7 damage and a new shape (360 degrees, double
148
+ radius, more count). Pair each base weapon with a different passive.
149
+
150
+ ### Add a passive
151
+ upgrade("iron_skin", { stat: "armor", step: 1, maxLevel: 4 });
152
+ stats: speed, maxHealth, magnet, might, haste, regen, armor, area (bigger
153
+ novas, auras, cones, meteors and orbits), amount (more projectiles, blades,
154
+ jumps and meteors). Up to 8. maxLevel caps how often it is offered.
155
+
156
+ ### Add pickups and loot
157
+ prefab("cake_slice", { model: "style.lowpoly.cell_heal", health: 1, speed: 0, kind: "pickup", pickup: { effect: "heal", amount: 0.3 } });
158
+ A boss's hoard: pickup: { effect: "chest", amount: 3, evolve: true }. Bosses
159
+ drop it instead of the plain chest. If the player holds a weapon and already
160
+ has its paired passive, that weapon jumps to max level, so its evolution is
161
+ on the cards.
162
+ effects: heal (amount: fraction of max health, or hit points above 1),
163
+ vacuum (every gem on the floor comes to you), bomb (clears the screen's
164
+ non-boss enemies), chest (amount card picks; a ready evolution shows first).
165
+ Drop them with a chance: drops: ["star_small", { id: "cake_slice", chance: 0.02 }].
166
+ Elites always drop the game's chest; bosses drop a heal, a vacuum and a chest.
167
+ Above 300 gems on the floor, new gems merge into the nearest one.
168
+
169
+ ### Add props (breakables)
170
+ prefab("gift_box", { model: "style.lowpoly.crate", health: 30, speed: 0, radius: 0.55, prop: true, drops: ["star_mid", { id: "cake_slice", chance: 0.2 }] });
171
+ then scatter("gift_box", 20) from a round's start() (props land on the floor, off every screen). Props never move, never hurt
172
+ and never count as enemies.
173
+
174
+ ### Add events and moments
175
+ In a round's start() or the show's next()/finish():
176
+ on("round.won", () => { if (random() < 0.3) { announce("ENCORE", { tone: "reward" }); xpBoost(2, 15); } });
177
+ announce tones: info, danger, reward. Events you can key on (and earn rules
178
+ too): round.won, show.perfect, miniboss.killed, player.downed,
179
+ player.revived, chest.collected, "<prefab>.killed". clear("popper")
180
+ removes a prefab at once (no kills or drops).
181
+
182
+ ### Cards
183
+ Weighted: an owned weapon's next level 8, a new weapon 10 (while a slot is
184
+ free), a passive 4 (7 once weapon slots are full); a ready evolution always
185
+ shows. players.rerolls (default 1) rerolls per run (R); Skip (X) trades the
186
+ cards for a small heal. When everything is maxed, a level heals and gives
187
+ teammates a little XP.
188
+
189
+ ### Add a boss
190
+ prefab("disco_dragon", {
191
+ model: "style.lowpoly.dragon_whelp", appearance: { tint: "#c9a7ff", scale: 1.4 }, health: 800, speed: 2.4, radius: 1.3, damage: 14,
192
+ boss: { attacks: ["slam", "leap"], rank: "mini" },
193
+ drops: ["star_large"],
194
+ });
195
+ Spawn it in a round's start() with spawnEdge("disco_dragon", { count: 1 }) and
196
+ win the round on on("miniboss.killed"). Attacks (all telegraphed): slam
197
+ (circle around it), dash (11 m lane), leap (lands on a circle at you), rain
198
+ (circles near players), ring (14 shots outward), fan (7 shots at you), summon
199
+ (adds). Near attacks (slam, leap) keep it after the players; a boss that only
200
+ throws from afar can be ignored by a party that will not chase it. Boss
201
+ health scales with players present (players.bossHealthPerPlayer, 0.35 here).
202
+
203
+ ### Add an earn rule
204
+ earn.rule("three_wins", { when: "round.won", n: 3, pay: 10, oncePer: "run" });
205
+ A run is one show. Events: round.won and show.perfect (the show emits them), enemy.killed (credits the killer),
206
+ boss.killed (everyone present), player.downed, player.revived (credits the
207
+ reviver), player.levelup, run.won, run.lost, and anything you emit().
208
+ Each rule pays once per run per player, in coins; the room server is the referee.
209
+
210
+ ### The buy-in run (required)
211
+ earn.stake({ min: 20, max: 200, multiple: 2 });
212
+ Every game declares one, on one top-level line; a game without it does not
213
+ build. A player stakes coins at the start of a run (the Kit spends them as
214
+ coins.spend(amount, "stake"), then the client calls conn.stake()). The room
215
+ fixes the stake when the next run starts. A staked run pays its normal earn
216
+ plus a share of stake x multiple: the share is the part of the run's most
217
+ earn the player makes. What it does not win back stays in the reward pool.
218
+ multiple is 1 to 2. "stake" is reserved: never a SKU or meta upgrade id.
219
+
220
+ ### Losable gear (optional)
221
+ store.sku("lucky_charm", { coins: 150, grants: { gear: "lucky_sock", loseBelow: 0.25 } });
222
+ Gear is owned in the player's profile and sells for coins only. It brings one
223
+ level of its declared upgrade into every staked run, and is lost when that run
224
+ makes less than loseBelow of its most earn. Its coins stay in the reward pool.
225
+
226
+ ### Coins: earn and the store
227
+
228
+ Every game has coins: 1 coin = 1 of the game's own token, and players see
229
+ them under the token's own name, symbol and logo (never "coins" or "tokens"
230
+ as a label). Earn rules pay coins (`pay` is whole coins). The recommended
231
+ coin model (the default, no declaration) sells cosmetics, unlocks and
232
+ in-run consumables, never permanent stat power:
233
+
234
+ store.sku("revive", { coins: 50 }); // in-run: used while downed
235
+ store.sku("skin_disco", { coins: 300, grants: { cosmetic: 1 } }); // a look, owned for good
236
+
237
+ Keep them and the lucky_charm gear exactly: the client is wired to them (price, look id and gear). Owned
238
+ looks and unlocks are saved on the room server in the player's profile. Players spend
239
+ earned coins first (instantly, no wallet prompt), then their wallet. Before
240
+ the game launches coins are play money; the Kit switches by itself, so
241
+ nothing in game.ts depends on it. Price in coins against what a run pays
242
+ (the four rules here pay up to 80 a run): a revive is under a run, a
243
+ cosmetic a few runs.
244
+
245
+ ### Sell a keeper unlock (a character, a starting loadout, a sidegrade)
246
+ keeper("mc", { name: "MC", text: "Streamers and a party hat.", weapon: "streamer", passive: "party_hat" }); // no unlock
247
+ store.sku("unlock_mc", { coins: 400, grants: { keeper: "mc" } });
248
+ A keeper is earned (unlock: an achievement) or bought (a SKU), never both:
249
+ the build refuses a sold keeper that also declares an unlock. Never sell
250
+ the first keeper (everyone's free default). An unlock is a different way to
251
+ play, never more power. Before launch coins are play money and buy only
252
+ in-run items; looks and unlocks open at launch.
253
+
254
+ ### Cosmetics only ("no pay to win")
255
+ economy({ store: "cosmetics" });
256
+ Then every SKU must be a look (grants: { cosmetic: n }); drop revive.
257
+
258
+ ### Permanent upgrades for coins (opt-in meta upgrades)
259
+ economy({ metaUpgrades: true });
260
+ metaUpgrade("hearth", { stat: "maxHealth", step: 0.05, levels: 3, cost: 120, growth: 2 }); // 120, 240, 480 coins
261
+ metaUpgrade("stride", { stat: "speed", step: 0.03, levels: 3, cost: 100, growth: 1.8 });
262
+ Only when the creator asks for permanent upgrades. Stats: speed, maxHealth,
263
+ magnet, might, haste, area. The engine refuses more than +15% to one stat
264
+ or +30% in all, more than 5 levels, or growth under 1.5, and applies a
265
+ player's levels at the start of every run. The store shows each level.
266
+
267
+ ### Add a keeper (a playable character)
268
+ keeper("juggler", { name: "Juggler", text: "Spinning plates and a wider spin.", model: "style.lowpoly.blocky_k", tint: "#7ad7ff", weapon: "spinning_plates", passive: "disco_ball", unlock: "first_hit" });
269
+ A keeper starts every run with its weapon and its passive at level 1;
270
+ players pick one before their first level and on the end screen. At most
271
+ 15; the first declared keeper is everyone's default, and at least one
272
+ keeper has no unlock (declare a free one first). Keepers unlock with
273
+ achievements, or with coins through a SKU with grants: { keeper }. The room
274
+ server checks every pick against the player's saved unlocks.
275
+ With keepers declared, players.weapon is optional.
276
+
277
+ ### Add achievements, daily goals and end-screen titles
278
+ achievement("evolver", { title: "Evolved", text: "Evolve a weapon.", when: "player.evolved" });
279
+ mission("elite", { text: "Kill an elite", when: "elite.killed" });
280
+ title("butcher", { text: "The Butcher", most: "kills" });
281
+ Achievements and missions count a game event per player in one run (n
282
+ times) and pay nothing (coins come only from earn rules). The room server
283
+ grants them from the run and saves them per player (wallet, or guest until
284
+ sign-in), so they follow the player to every device. The HUD shows
285
+ three missions a day. Titles rank players at the end of a run by most or
286
+ least of: kills, damage, revives, downs, level, pickups. Events to count:
287
+ anything you emit, "<prefab>.killed" ("party_pup.killed"),
288
+ "elite.killed", "player.evolved", "player.revived", "chest.collected",
289
+ "miniboss.killed", "round.won", "show.perfect".
290
+
291
+ ### Co-op that feels shared
292
+ players: { share: "nearby" } // default: half of each gem to teammates within 20 m
293
+ players: { share: "all" } // or "collector", or { mode, fraction, radius }
294
+ players: { reviveBuff: { seconds: 5, damage: 1.3, speed: 1.15 } } // the reviver burns hotter
295
+ players: { downed: { seconds: 8, crawl: 0.5, selfRevives: 2, respawn: 3 } } // this game: forgiving
296
+ players: { lateJoinBehind: 2 } // a late joiner starts 2 levels under the team, cards waiting
297
+ players: { crit: { chance: 0.08, multiplier: 2 } } // crits show as gold numbers
298
+ The end screen votes "Play again" (everyone ready starts the next run; it
299
+ starts by itself after 10 s) and shows each keeper's build, damage per
300
+ weapon, what downed them, coins and titles.
301
+
302
+ ### Players per room
303
+ players.max is 8 here, the most the hosted Net Forge room server seats
304
+ in a party room. Fewer is fine; more is refused.
305
+
306
+ ## Models and appearances (the asset library)
307
+
308
+ Every model is a real CC0 model from the asset library, by id. Enemies are
309
+ drawn about 3.2 hit radii tall, keepers 1.8 m; animated ones walk, run and
310
+ die by themselves. An appearance dresses a model into your own original:
311
+ appearance: { tint: "#rrggbb", accent: "#rrggbb", scale: 0.4 to 3, parts: up to 4 }.
312
+
313
+ ### Make the crowd look like robots
314
+ prefab("popper", { model: "style.lowpoly.robot_walker", ... });
315
+ prefab("fizz", { model: "style.lowpoly.robot_flyer", ... });
316
+ Keep the ids or rename them everywhere (ROUNDS and the sweep list too), and
317
+ retell it in names.json.
318
+
319
+ ### A hero described by the creator
320
+ players: { prefab: "style.lowpoly.worker", appearance: { tint: "#2e7d32", accent: "#3b2a1a", parts: ["mustache", "hat_cap"] }, ... }
321
+ Name it something new in names.json. A creature the library lacks: the
322
+ nearest base plus parts, e.g. model "style.lowpoly.shiba" with
323
+ appearance { tint: "#5ec8ff", parts: ["antenna", "tail_bolt"] }.
324
+
325
+ ## Look and feel (engine render stack and feel kit)
326
+
327
+ How the game looks and how hits feel are declarations too. They are
328
+ presentation only (never the simulation), so tune them freely. The full
329
+ reference is packages/engine/llms.txt.
330
+
331
+ ### Change the look
332
+ game({ ..., look: { preset: "dusk", bloom: 1.4, fog: 0.3, tint: "#ffd9a0" } });
333
+ presets: "night" (default), "dusk", "day", "studio". Also shadows (off here unless
334
+ look.shadows is true), outline,
335
+ exposure, saturation, contrast, vignette.
336
+
337
+ ### Make a death or a hit feel bigger
338
+ prefab("brute", { ..., feel: { particles: "burst", decal: "scorch", sound: "boom", shake: 0.5, hitStop: 0.06, color: "#ff4d5e" } });
339
+ Without feel, big enemies (bosses, radius 0.5 m or more) burst, scorch and
340
+ shake, and small ones burst, splat and crunch.
341
+
342
+ ### Make a weapon's impacts feel different
343
+ weapon("frost_ring", { ..., feel: { particles: "magic", decal: "ring", color: "#7ad7ff", shake: 0.1 } });
344
+ Novas use it when they go off; bolts and lances when they hit.
345
+
346
+ ### Heavier or lighter knockback
347
+ weapon knockback (m/s) pushes enemies, divided by their mass. Knocked
348
+ enemies stagger, slide, bounce off walls and pillars and shove the
349
+ enemies behind them. Bolts stop at walls and pillars.
350
+
351
+ ## Systems API
352
+
353
+ every(seconds, n => ...), after(seconds, fn), onTick(fn), spawn(prefab, [x,y,z], { elite? }),
354
+ spawnPickup(prefab, [x,y,z]), announce(text, { tone }), bossAlive(),
355
+ pauseDirector(seconds), surge(multiplier, seconds), xpBoost(multiplier, seconds),
356
+ scatter(prefab, count), clear(prefab), upcoming(label, seconds) (a HUD countdown:
357
+ upcoming("THE BELL WARDEN", 240)),
358
+ spawnEdge(prefab, { count, around?, side?, spread?, scale? }) (just off screen;
359
+ side: "any" | "ahead" | "behind" | "north" | "south" | "east" | "west"),
360
+ spawnRing(prefab, { count, radius, around? }), alive(prefab?), players(),
361
+ emit(event), on(event, players => ...), respawnDead(), end("won" | "lost"),
362
+ onScreen(x, z) (true when a player who is up can see that ground point),
363
+ random(), tick(), time().
364
+
365
+ ## Test
366
+
367
+ engine.test.run({ ticks: 30 * 180, bots: 4, assert: [...] }) plays the game
368
+ on the server sim with bots and reports kills, wave, maxLevel, earned by
369
+ rule, deaths, runs won and lost, softlock and ms per tick (wave is
370
+ 1 + rounds played). With
371
+ bot: "human" a human-like bot plays (200 ms reactions, kiting, a second per
372
+ card) and report.fun has the fun metrics: first threat, first level,
373
+ level at 1/3/6/9 minutes, downs before 1:00, enemies on screen at 0:30,
374
+ touches in minute 1, spawns on screen, survival. Net Forge's gate scores
375
+ them; `pnpm --filter @netforge/template-party fun` prints the table.
package/package.json CHANGED
@@ -1,6 +1,85 @@
1
1
  {
2
2
  "name": "@netforge/template-party",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Party game template for Net Forge: 2 to 8 friends in one playroom, five quick minigame rounds a show (ROMPHOUSE).",
5
+ "keywords": [
6
+ "game-template",
7
+ "multiplayer",
8
+ "browser-game",
9
+ "html5-game",
10
+ "threejs",
11
+ "game-engine",
12
+ "typescript",
13
+ "gamedev",
14
+ "ai-agents",
15
+ "mcp",
16
+ "net-forge",
17
+ "party-game"
18
+ ],
19
+ "license": "MIT",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": "./src/manifest.ts",
23
+ "./store": "./src/store.ts",
24
+ "./package.json": "./package.json",
25
+ "./llms.txt": "./llms.txt",
26
+ "./llms-full.txt": "./llms-full.txt"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "client",
31
+ "!client/vite.config.ts",
32
+ "llms.txt",
33
+ "README.md",
34
+ "NOTICE",
35
+ "LICENSE",
36
+ "llms-full.txt",
37
+ "AGENTS.md",
38
+ "CLAUDE.md"
39
+ ],
40
+ "sideEffects": [
41
+ "./src/game.ts",
42
+ "./src/manifest.ts",
43
+ "./client/**"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "homepage": "https://forge.netnet.capital/docs/templates/party",
49
+ "bugs": {
50
+ "url": "https://github.com/netnetdev/net-forge/issues"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/netnetdev/net-forge.git",
55
+ "directory": "packages/template-party"
56
+ },
57
+ "engines": {
58
+ "node": ">=20"
59
+ },
60
+ "dependencies": {
61
+ "nipplejs": "1.0.4",
62
+ "three": "0.186.1",
63
+ "@netforge/asset-library": "0.2.0",
64
+ "@netforge/engine-sim": "0.2.0",
65
+ "@netforge/engine": "0.2.0"
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "22.20.1",
69
+ "@types/three": "0.186.0",
70
+ "tsx": "4.23.0",
71
+ "typescript": "5.8.3",
72
+ "vite": "6.4.3",
73
+ "vitest": "3.2.7"
74
+ },
75
+ "scripts": {
76
+ "build": "node ../engine/scripts/fresh-dist.mjs && vite build --config client/vite.config.ts",
77
+ "typecheck": "tsc -p tsconfig.json",
78
+ "test": "vitest run",
79
+ "dev": "vite --config client/vite.config.ts",
80
+ "preview": "vite preview --config client/vite.config.ts",
81
+ "bots": "tsx scripts/load-bots.ts",
82
+ "fun": "tsx scripts/fun.ts",
83
+ "bench": "tsx scripts/bench.ts"
84
+ }
6
85
  }