@rune-kit/rune 2.10.0 → 2.12.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/LICENSE +21 -21
- package/README.md +65 -6
- package/commands/rune.md +168 -168
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +118 -0
- package/compiler/__tests__/hooks-cursor.test.js +139 -0
- package/compiler/__tests__/hooks-install.test.js +305 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +519 -0
- package/compiler/__tests__/hooks-windsurf.test.js +115 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +221 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +380 -342
- package/skills/browser-pilot/SKILL.md +169 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +408 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +590 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +178 -177
- package/skills/fix/SKILL.md +332 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +220 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +241 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +269 -251
- package/skills/marketing/SKILL.md +351 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +359 -362
- package/skills/onboard/SKILL.md +432 -403
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +194 -0
- package/skills/perf/SKILL.md +347 -346
- package/skills/plan/SKILL.md +435 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/research/SKILL.md +4 -0
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +614 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +183 -180
- package/skills/scout/SKILL.md +269 -263
- package/skills/sentinel/SKILL.md +384 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +595 -543
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +557 -537
- package/skills/test/SKILL.md +620 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +334 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "multiplayer"
|
|
3
|
-
pack: "@rune/gamedev"
|
|
4
|
-
description: "WebSocket game server and client prediction — authoritative server model, client-side prediction, reconciliation, entity interpolation, and lag compensation."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# multiplayer
|
|
10
|
-
|
|
11
|
-
WebSocket game server and client prediction — authoritative server model, client-side prediction, reconciliation, entity interpolation, and lag compensation.
|
|
12
|
-
|
|
13
|
-
Authoritative server model: server owns game state, clients send inputs only. Never trust client position.
|
|
14
|
-
|
|
15
|
-
#### Workflow
|
|
16
|
-
|
|
17
|
-
**Step 1 — Assess multiplayer architecture**
|
|
18
|
-
Use Grep to find existing WebSocket code: `WebSocket`, `socket.io`, `ws`, `onmessage`, `postMessage`. Determine: is there a server or is this client-only? What is the tick rate?
|
|
19
|
-
|
|
20
|
-
**Step 2 — Implement authoritative server**
|
|
21
|
-
Emit: Node.js WebSocket server with fixed-tick update (20Hz sufficient), input queue per player, server-side physics/movement, state broadcast.
|
|
22
|
-
|
|
23
|
-
**Step 3 — Implement client prediction**
|
|
24
|
-
Emit: local input application before server confirmation, pending input buffer, reconciliation on server update, smooth interpolation for remote entities.
|
|
25
|
-
|
|
26
|
-
**Step 4 — Add lag compensation**
|
|
27
|
-
Emit: snapshot buffer for entity interpolation (render ~100ms behind server), client prediction with rollback on desync.
|
|
28
|
-
|
|
29
|
-
#### WebSocket Game Server Pattern
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
// Server (Node.js + ws) — authoritative game server
|
|
33
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
34
|
-
|
|
35
|
-
interface PlayerInput { seq: number; keys: { up: boolean; down: boolean; left: boolean; right: boolean }; }
|
|
36
|
-
interface PlayerState { id: string; x: number; y: number; vx: number; vy: number; }
|
|
37
|
-
|
|
38
|
-
const wss = new WebSocketServer({ port: 3001 });
|
|
39
|
-
const players = new Map<string, PlayerState>();
|
|
40
|
-
const inputQueues = new Map<string, PlayerInput[]>();
|
|
41
|
-
|
|
42
|
-
wss.on('connection', (ws: WebSocket, req) => {
|
|
43
|
-
const id = crypto.randomUUID();
|
|
44
|
-
players.set(id, { id, x: 0, y: 0, vx: 0, vy: 0 });
|
|
45
|
-
inputQueues.set(id, []);
|
|
46
|
-
|
|
47
|
-
ws.send(JSON.stringify({ type: 'init', id, state: Object.fromEntries(players) }));
|
|
48
|
-
broadcast({ type: 'player_joined', id });
|
|
49
|
-
|
|
50
|
-
ws.on('message', (raw) => {
|
|
51
|
-
const msg = JSON.parse(raw.toString());
|
|
52
|
-
if (msg.type === 'input') {
|
|
53
|
-
inputQueues.get(id)?.push(msg.input);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
ws.on('close', () => {
|
|
58
|
-
players.delete(id);
|
|
59
|
-
inputQueues.delete(id);
|
|
60
|
-
broadcast({ type: 'player_left', id });
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// Fixed-tick server update (20Hz is sufficient for authoritative server)
|
|
65
|
-
const TICK_MS = 50;
|
|
66
|
-
setInterval(() => {
|
|
67
|
-
inputQueues.forEach((queue, id) => {
|
|
68
|
-
const player = players.get(id)!;
|
|
69
|
-
const input = queue.shift(); // process one input per tick
|
|
70
|
-
if (input) applyInput(player, input);
|
|
71
|
-
integratePhysics(player);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
broadcast({ type: 'state_update', tick: Date.now(), players: Object.fromEntries(players) });
|
|
75
|
-
}, TICK_MS);
|
|
76
|
-
|
|
77
|
-
function broadcast(msg: object) {
|
|
78
|
-
const data = JSON.stringify(msg);
|
|
79
|
-
wss.clients.forEach(c => c.readyState === WebSocket.OPEN && c.send(data));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function applyInput(p: PlayerState, input: PlayerInput) {
|
|
83
|
-
const speed = 200;
|
|
84
|
-
p.vx = (input.keys.right ? 1 : 0) - (input.keys.left ? 1 : 0);
|
|
85
|
-
p.vy = (input.keys.down ? 1 : 0) - (input.keys.up ? 1 : 0);
|
|
86
|
-
const len = Math.hypot(p.vx, p.vy);
|
|
87
|
-
if (len > 0) { p.vx = (p.vx / len) * speed; p.vy = (p.vy / len) * speed; }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function integratePhysics(p: PlayerState) {
|
|
91
|
-
p.x += p.vx * (TICK_MS / 1000);
|
|
92
|
-
p.y += p.vy * (TICK_MS / 1000);
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
#### Client Prediction & Reconciliation
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
// Client — predict locally, reconcile on server update
|
|
100
|
-
class NetworkedPlayer {
|
|
101
|
-
private pendingInputs: { seq: number; input: PlayerInput; }[] = [];
|
|
102
|
-
private seq = 0;
|
|
103
|
-
localState: PlayerState;
|
|
104
|
-
serverState: PlayerState;
|
|
105
|
-
|
|
106
|
-
constructor(initial: PlayerState) {
|
|
107
|
-
this.localState = { ...initial };
|
|
108
|
-
this.serverState = { ...initial };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
sendInput(keys: PlayerInput['keys'], ws: WebSocket) {
|
|
112
|
-
const input: PlayerInput = { seq: ++this.seq, keys };
|
|
113
|
-
this.pendingInputs.push({ seq: this.seq, input });
|
|
114
|
-
ws.send(JSON.stringify({ type: 'input', input }));
|
|
115
|
-
|
|
116
|
-
// Apply immediately (client prediction)
|
|
117
|
-
applyInputToState(this.localState, keys);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
reconcile(serverUpdate: PlayerState & { lastProcessedSeq: number }) {
|
|
121
|
-
this.serverState = serverUpdate;
|
|
122
|
-
|
|
123
|
-
// Remove acknowledged inputs
|
|
124
|
-
this.pendingInputs = this.pendingInputs.filter(p => p.seq > serverUpdate.lastProcessedSeq);
|
|
125
|
-
|
|
126
|
-
// Reapply unacknowledged inputs on top of server state
|
|
127
|
-
this.localState = { ...serverUpdate };
|
|
128
|
-
for (const { input } of this.pendingInputs) {
|
|
129
|
-
applyInputToState(this.localState, input.keys);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function applyInputToState(state: PlayerState, keys: PlayerInput['keys']) {
|
|
135
|
-
const dt = 1 / 60;
|
|
136
|
-
const speed = 200;
|
|
137
|
-
state.x += ((keys.right ? 1 : 0) - (keys.left ? 1 : 0)) * speed * dt;
|
|
138
|
-
state.y += ((keys.down ? 1 : 0) - (keys.up ? 1 : 0)) * speed * dt;
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### Lag Compensation & Entity Interpolation
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
// Interpolate remote entities between server snapshots (smooth movement, ~100ms behind)
|
|
146
|
-
interface Snapshot { tick: number; timestamp: number; entities: Map<string, PlayerState>; }
|
|
147
|
-
|
|
148
|
-
class EntityInterpolator {
|
|
149
|
-
private buffer: Snapshot[] = [];
|
|
150
|
-
private readonly delay = 100; // ms behind server
|
|
151
|
-
|
|
152
|
-
addSnapshot(snapshot: Snapshot) {
|
|
153
|
-
this.buffer.push(snapshot);
|
|
154
|
-
// Keep only last 1 second of snapshots
|
|
155
|
-
const cutoff = Date.now() - 1000;
|
|
156
|
-
this.buffer = this.buffer.filter(s => s.timestamp > cutoff);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
getInterpolatedState(entityId: string): PlayerState | null {
|
|
160
|
-
const renderTime = Date.now() - this.delay;
|
|
161
|
-
|
|
162
|
-
// Find the two snapshots bracketing renderTime
|
|
163
|
-
const newer = this.buffer.find(s => s.timestamp >= renderTime);
|
|
164
|
-
const older = this.buffer.slice().reverse().find(s => s.timestamp < renderTime);
|
|
165
|
-
|
|
166
|
-
if (!older || !newer) return newer?.entities.get(entityId) ?? null;
|
|
167
|
-
|
|
168
|
-
const t = (renderTime - older.timestamp) / (newer.timestamp - older.timestamp);
|
|
169
|
-
const a = older.entities.get(entityId);
|
|
170
|
-
const b = newer.entities.get(entityId);
|
|
171
|
-
if (!a || !b) return null;
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
...a,
|
|
175
|
-
x: a.x + (b.x - a.x) * t,
|
|
176
|
-
y: a.y + (b.y - a.y) * t,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "multiplayer"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "WebSocket game server and client prediction — authoritative server model, client-side prediction, reconciliation, entity interpolation, and lag compensation."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# multiplayer
|
|
10
|
+
|
|
11
|
+
WebSocket game server and client prediction — authoritative server model, client-side prediction, reconciliation, entity interpolation, and lag compensation.
|
|
12
|
+
|
|
13
|
+
Authoritative server model: server owns game state, clients send inputs only. Never trust client position.
|
|
14
|
+
|
|
15
|
+
#### Workflow
|
|
16
|
+
|
|
17
|
+
**Step 1 — Assess multiplayer architecture**
|
|
18
|
+
Use Grep to find existing WebSocket code: `WebSocket`, `socket.io`, `ws`, `onmessage`, `postMessage`. Determine: is there a server or is this client-only? What is the tick rate?
|
|
19
|
+
|
|
20
|
+
**Step 2 — Implement authoritative server**
|
|
21
|
+
Emit: Node.js WebSocket server with fixed-tick update (20Hz sufficient), input queue per player, server-side physics/movement, state broadcast.
|
|
22
|
+
|
|
23
|
+
**Step 3 — Implement client prediction**
|
|
24
|
+
Emit: local input application before server confirmation, pending input buffer, reconciliation on server update, smooth interpolation for remote entities.
|
|
25
|
+
|
|
26
|
+
**Step 4 — Add lag compensation**
|
|
27
|
+
Emit: snapshot buffer for entity interpolation (render ~100ms behind server), client prediction with rollback on desync.
|
|
28
|
+
|
|
29
|
+
#### WebSocket Game Server Pattern
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Server (Node.js + ws) — authoritative game server
|
|
33
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
34
|
+
|
|
35
|
+
interface PlayerInput { seq: number; keys: { up: boolean; down: boolean; left: boolean; right: boolean }; }
|
|
36
|
+
interface PlayerState { id: string; x: number; y: number; vx: number; vy: number; }
|
|
37
|
+
|
|
38
|
+
const wss = new WebSocketServer({ port: 3001 });
|
|
39
|
+
const players = new Map<string, PlayerState>();
|
|
40
|
+
const inputQueues = new Map<string, PlayerInput[]>();
|
|
41
|
+
|
|
42
|
+
wss.on('connection', (ws: WebSocket, req) => {
|
|
43
|
+
const id = crypto.randomUUID();
|
|
44
|
+
players.set(id, { id, x: 0, y: 0, vx: 0, vy: 0 });
|
|
45
|
+
inputQueues.set(id, []);
|
|
46
|
+
|
|
47
|
+
ws.send(JSON.stringify({ type: 'init', id, state: Object.fromEntries(players) }));
|
|
48
|
+
broadcast({ type: 'player_joined', id });
|
|
49
|
+
|
|
50
|
+
ws.on('message', (raw) => {
|
|
51
|
+
const msg = JSON.parse(raw.toString());
|
|
52
|
+
if (msg.type === 'input') {
|
|
53
|
+
inputQueues.get(id)?.push(msg.input);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
ws.on('close', () => {
|
|
58
|
+
players.delete(id);
|
|
59
|
+
inputQueues.delete(id);
|
|
60
|
+
broadcast({ type: 'player_left', id });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Fixed-tick server update (20Hz is sufficient for authoritative server)
|
|
65
|
+
const TICK_MS = 50;
|
|
66
|
+
setInterval(() => {
|
|
67
|
+
inputQueues.forEach((queue, id) => {
|
|
68
|
+
const player = players.get(id)!;
|
|
69
|
+
const input = queue.shift(); // process one input per tick
|
|
70
|
+
if (input) applyInput(player, input);
|
|
71
|
+
integratePhysics(player);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
broadcast({ type: 'state_update', tick: Date.now(), players: Object.fromEntries(players) });
|
|
75
|
+
}, TICK_MS);
|
|
76
|
+
|
|
77
|
+
function broadcast(msg: object) {
|
|
78
|
+
const data = JSON.stringify(msg);
|
|
79
|
+
wss.clients.forEach(c => c.readyState === WebSocket.OPEN && c.send(data));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function applyInput(p: PlayerState, input: PlayerInput) {
|
|
83
|
+
const speed = 200;
|
|
84
|
+
p.vx = (input.keys.right ? 1 : 0) - (input.keys.left ? 1 : 0);
|
|
85
|
+
p.vy = (input.keys.down ? 1 : 0) - (input.keys.up ? 1 : 0);
|
|
86
|
+
const len = Math.hypot(p.vx, p.vy);
|
|
87
|
+
if (len > 0) { p.vx = (p.vx / len) * speed; p.vy = (p.vy / len) * speed; }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function integratePhysics(p: PlayerState) {
|
|
91
|
+
p.x += p.vx * (TICK_MS / 1000);
|
|
92
|
+
p.y += p.vy * (TICK_MS / 1000);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Client Prediction & Reconciliation
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// Client — predict locally, reconcile on server update
|
|
100
|
+
class NetworkedPlayer {
|
|
101
|
+
private pendingInputs: { seq: number; input: PlayerInput; }[] = [];
|
|
102
|
+
private seq = 0;
|
|
103
|
+
localState: PlayerState;
|
|
104
|
+
serverState: PlayerState;
|
|
105
|
+
|
|
106
|
+
constructor(initial: PlayerState) {
|
|
107
|
+
this.localState = { ...initial };
|
|
108
|
+
this.serverState = { ...initial };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
sendInput(keys: PlayerInput['keys'], ws: WebSocket) {
|
|
112
|
+
const input: PlayerInput = { seq: ++this.seq, keys };
|
|
113
|
+
this.pendingInputs.push({ seq: this.seq, input });
|
|
114
|
+
ws.send(JSON.stringify({ type: 'input', input }));
|
|
115
|
+
|
|
116
|
+
// Apply immediately (client prediction)
|
|
117
|
+
applyInputToState(this.localState, keys);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
reconcile(serverUpdate: PlayerState & { lastProcessedSeq: number }) {
|
|
121
|
+
this.serverState = serverUpdate;
|
|
122
|
+
|
|
123
|
+
// Remove acknowledged inputs
|
|
124
|
+
this.pendingInputs = this.pendingInputs.filter(p => p.seq > serverUpdate.lastProcessedSeq);
|
|
125
|
+
|
|
126
|
+
// Reapply unacknowledged inputs on top of server state
|
|
127
|
+
this.localState = { ...serverUpdate };
|
|
128
|
+
for (const { input } of this.pendingInputs) {
|
|
129
|
+
applyInputToState(this.localState, input.keys);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function applyInputToState(state: PlayerState, keys: PlayerInput['keys']) {
|
|
135
|
+
const dt = 1 / 60;
|
|
136
|
+
const speed = 200;
|
|
137
|
+
state.x += ((keys.right ? 1 : 0) - (keys.left ? 1 : 0)) * speed * dt;
|
|
138
|
+
state.y += ((keys.down ? 1 : 0) - (keys.up ? 1 : 0)) * speed * dt;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Lag Compensation & Entity Interpolation
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Interpolate remote entities between server snapshots (smooth movement, ~100ms behind)
|
|
146
|
+
interface Snapshot { tick: number; timestamp: number; entities: Map<string, PlayerState>; }
|
|
147
|
+
|
|
148
|
+
class EntityInterpolator {
|
|
149
|
+
private buffer: Snapshot[] = [];
|
|
150
|
+
private readonly delay = 100; // ms behind server
|
|
151
|
+
|
|
152
|
+
addSnapshot(snapshot: Snapshot) {
|
|
153
|
+
this.buffer.push(snapshot);
|
|
154
|
+
// Keep only last 1 second of snapshots
|
|
155
|
+
const cutoff = Date.now() - 1000;
|
|
156
|
+
this.buffer = this.buffer.filter(s => s.timestamp > cutoff);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getInterpolatedState(entityId: string): PlayerState | null {
|
|
160
|
+
const renderTime = Date.now() - this.delay;
|
|
161
|
+
|
|
162
|
+
// Find the two snapshots bracketing renderTime
|
|
163
|
+
const newer = this.buffer.find(s => s.timestamp >= renderTime);
|
|
164
|
+
const older = this.buffer.slice().reverse().find(s => s.timestamp < renderTime);
|
|
165
|
+
|
|
166
|
+
if (!older || !newer) return newer?.entities.get(entityId) ?? null;
|
|
167
|
+
|
|
168
|
+
const t = (renderTime - older.timestamp) / (newer.timestamp - older.timestamp);
|
|
169
|
+
const a = older.entities.get(entityId);
|
|
170
|
+
const b = newer.entities.get(entityId);
|
|
171
|
+
if (!a || !b) return null;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...a,
|
|
175
|
+
x: a.x + (b.x - a.x) * t,
|
|
176
|
+
y: a.y + (b.y - a.y) * t,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: "particles"
|
|
3
|
-
pack: "@rune/gamedev"
|
|
4
|
-
description: "GPU particle system with WebGL instancing — object-pooled particles, emission presets, and performance-safe rendering for 10k+ particles."
|
|
5
|
-
model: sonnet
|
|
6
|
-
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# particles
|
|
10
|
-
|
|
11
|
-
GPU particle system with WebGL instancing — object-pooled particles, emission presets, and performance-safe rendering for 10k+ particles.
|
|
12
|
-
|
|
13
|
-
#### Workflow
|
|
14
|
-
|
|
15
|
-
**Step 1 — Detect particle usage**
|
|
16
|
-
Use Grep to find particle code: `particles`, `emitter`, `ParticleSystem`, `createBufferSource`. Understand current implementation: CPU vs GPU, pooling strategy, and draw call count.
|
|
17
|
-
|
|
18
|
-
**Step 2 — Audit particle performance**
|
|
19
|
-
Check for: new particle objects created on emit (GC pressure), no object pool, drawing each particle as a separate draw call, no life/alpha fade.
|
|
20
|
-
|
|
21
|
-
**Step 3 — Emit particle system**
|
|
22
|
-
Emit: ParticleSystem with pre-allocated pool, update/render separation, emitter presets for common effects (explosion, sparks, smoke).
|
|
23
|
-
|
|
24
|
-
#### GPU Particles with WebGL Instancing
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
// GPU particle system — update on CPU, render 10k+ particles via instancing
|
|
28
|
-
interface Particle {
|
|
29
|
-
x: number; y: number;
|
|
30
|
-
vx: number; vy: number;
|
|
31
|
-
life: number; maxLife: number;
|
|
32
|
-
size: number;
|
|
33
|
-
r: number; g: number; b: number; a: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
class ParticleSystem {
|
|
37
|
-
private particles: Particle[] = [];
|
|
38
|
-
private pool: Particle[] = [];
|
|
39
|
-
private readonly maxParticles: number;
|
|
40
|
-
|
|
41
|
-
constructor(maxParticles = 5000) {
|
|
42
|
-
this.maxParticles = maxParticles;
|
|
43
|
-
// Pre-allocate pool
|
|
44
|
-
for (let i = 0; i < maxParticles; i++) {
|
|
45
|
-
this.pool.push({ x:0,y:0,vx:0,vy:0,life:0,maxLife:1,size:4,r:1,g:1,b:1,a:1 });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
emit(x: number, y: number, count: number, config: Partial<Particle> = {}) {
|
|
50
|
-
for (let i = 0; i < count && this.particles.length < this.maxParticles; i++) {
|
|
51
|
-
const p = this.pool.pop() ?? { x:0,y:0,vx:0,vy:0,life:0,maxLife:1,size:4,r:1,g:1,b:1,a:1 };
|
|
52
|
-
const angle = Math.random() * Math.PI * 2;
|
|
53
|
-
const speed = 50 + Math.random() * 150;
|
|
54
|
-
Object.assign(p, {
|
|
55
|
-
x, y,
|
|
56
|
-
vx: Math.cos(angle) * speed,
|
|
57
|
-
vy: Math.sin(angle) * speed,
|
|
58
|
-
life: 1, maxLife: 0.5 + Math.random() * 1.5,
|
|
59
|
-
size: 3 + Math.random() * 5,
|
|
60
|
-
r: 1, g: 0.5, b: 0.1, a: 1,
|
|
61
|
-
...config,
|
|
62
|
-
});
|
|
63
|
-
this.particles.push(p);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
update(dt: number) {
|
|
68
|
-
const gravity = 200;
|
|
69
|
-
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
70
|
-
const p = this.particles[i];
|
|
71
|
-
p.x += p.vx * dt;
|
|
72
|
-
p.y += p.vy * dt;
|
|
73
|
-
p.vy += gravity * dt;
|
|
74
|
-
p.life -= dt / p.maxLife;
|
|
75
|
-
p.a = Math.max(0, p.life);
|
|
76
|
-
if (p.life <= 0) {
|
|
77
|
-
this.pool.push(p);
|
|
78
|
-
this.particles.splice(i, 1);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Render with Canvas 2D (swap with WebGL instancing for >10k particles)
|
|
84
|
-
render(ctx: CanvasRenderingContext2D) {
|
|
85
|
-
for (const p of this.particles) {
|
|
86
|
-
ctx.globalAlpha = p.a;
|
|
87
|
-
ctx.fillStyle = `rgb(${p.r * 255 | 0},${p.g * 255 | 0},${p.b * 255 | 0})`;
|
|
88
|
-
ctx.beginPath();
|
|
89
|
-
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
|
|
90
|
-
ctx.fill();
|
|
91
|
-
}
|
|
92
|
-
ctx.globalAlpha = 1;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Emitter presets
|
|
97
|
-
const EMITTER_PRESETS = {
|
|
98
|
-
explosion: (x: number, y: number, sys: ParticleSystem) =>
|
|
99
|
-
sys.emit(x, y, 80, { r: 1, g: 0.4, b: 0 }),
|
|
100
|
-
sparks: (x: number, y: number, sys: ParticleSystem) =>
|
|
101
|
-
sys.emit(x, y, 20, { r: 1, g: 0.9, b: 0.2, size: 2, maxLife: 0.8 }),
|
|
102
|
-
smoke: (x: number, y: number, sys: ParticleSystem) =>
|
|
103
|
-
sys.emit(x, y, 10, { r: 0.5, g: 0.5, b: 0.5, size: 12, maxLife: 3, vx: 0, vy: -30 }),
|
|
104
|
-
};
|
|
105
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: "particles"
|
|
3
|
+
pack: "@rune/gamedev"
|
|
4
|
+
description: "GPU particle system with WebGL instancing — object-pooled particles, emission presets, and performance-safe rendering for 10k+ particles."
|
|
5
|
+
model: sonnet
|
|
6
|
+
tools: [Read, Edit, Write, Grep, Glob, Bash]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# particles
|
|
10
|
+
|
|
11
|
+
GPU particle system with WebGL instancing — object-pooled particles, emission presets, and performance-safe rendering for 10k+ particles.
|
|
12
|
+
|
|
13
|
+
#### Workflow
|
|
14
|
+
|
|
15
|
+
**Step 1 — Detect particle usage**
|
|
16
|
+
Use Grep to find particle code: `particles`, `emitter`, `ParticleSystem`, `createBufferSource`. Understand current implementation: CPU vs GPU, pooling strategy, and draw call count.
|
|
17
|
+
|
|
18
|
+
**Step 2 — Audit particle performance**
|
|
19
|
+
Check for: new particle objects created on emit (GC pressure), no object pool, drawing each particle as a separate draw call, no life/alpha fade.
|
|
20
|
+
|
|
21
|
+
**Step 3 — Emit particle system**
|
|
22
|
+
Emit: ParticleSystem with pre-allocated pool, update/render separation, emitter presets for common effects (explosion, sparks, smoke).
|
|
23
|
+
|
|
24
|
+
#### GPU Particles with WebGL Instancing
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// GPU particle system — update on CPU, render 10k+ particles via instancing
|
|
28
|
+
interface Particle {
|
|
29
|
+
x: number; y: number;
|
|
30
|
+
vx: number; vy: number;
|
|
31
|
+
life: number; maxLife: number;
|
|
32
|
+
size: number;
|
|
33
|
+
r: number; g: number; b: number; a: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class ParticleSystem {
|
|
37
|
+
private particles: Particle[] = [];
|
|
38
|
+
private pool: Particle[] = [];
|
|
39
|
+
private readonly maxParticles: number;
|
|
40
|
+
|
|
41
|
+
constructor(maxParticles = 5000) {
|
|
42
|
+
this.maxParticles = maxParticles;
|
|
43
|
+
// Pre-allocate pool
|
|
44
|
+
for (let i = 0; i < maxParticles; i++) {
|
|
45
|
+
this.pool.push({ x:0,y:0,vx:0,vy:0,life:0,maxLife:1,size:4,r:1,g:1,b:1,a:1 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
emit(x: number, y: number, count: number, config: Partial<Particle> = {}) {
|
|
50
|
+
for (let i = 0; i < count && this.particles.length < this.maxParticles; i++) {
|
|
51
|
+
const p = this.pool.pop() ?? { x:0,y:0,vx:0,vy:0,life:0,maxLife:1,size:4,r:1,g:1,b:1,a:1 };
|
|
52
|
+
const angle = Math.random() * Math.PI * 2;
|
|
53
|
+
const speed = 50 + Math.random() * 150;
|
|
54
|
+
Object.assign(p, {
|
|
55
|
+
x, y,
|
|
56
|
+
vx: Math.cos(angle) * speed,
|
|
57
|
+
vy: Math.sin(angle) * speed,
|
|
58
|
+
life: 1, maxLife: 0.5 + Math.random() * 1.5,
|
|
59
|
+
size: 3 + Math.random() * 5,
|
|
60
|
+
r: 1, g: 0.5, b: 0.1, a: 1,
|
|
61
|
+
...config,
|
|
62
|
+
});
|
|
63
|
+
this.particles.push(p);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
update(dt: number) {
|
|
68
|
+
const gravity = 200;
|
|
69
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
70
|
+
const p = this.particles[i];
|
|
71
|
+
p.x += p.vx * dt;
|
|
72
|
+
p.y += p.vy * dt;
|
|
73
|
+
p.vy += gravity * dt;
|
|
74
|
+
p.life -= dt / p.maxLife;
|
|
75
|
+
p.a = Math.max(0, p.life);
|
|
76
|
+
if (p.life <= 0) {
|
|
77
|
+
this.pool.push(p);
|
|
78
|
+
this.particles.splice(i, 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Render with Canvas 2D (swap with WebGL instancing for >10k particles)
|
|
84
|
+
render(ctx: CanvasRenderingContext2D) {
|
|
85
|
+
for (const p of this.particles) {
|
|
86
|
+
ctx.globalAlpha = p.a;
|
|
87
|
+
ctx.fillStyle = `rgb(${p.r * 255 | 0},${p.g * 255 | 0},${p.b * 255 | 0})`;
|
|
88
|
+
ctx.beginPath();
|
|
89
|
+
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
|
|
90
|
+
ctx.fill();
|
|
91
|
+
}
|
|
92
|
+
ctx.globalAlpha = 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Emitter presets
|
|
97
|
+
const EMITTER_PRESETS = {
|
|
98
|
+
explosion: (x: number, y: number, sys: ParticleSystem) =>
|
|
99
|
+
sys.emit(x, y, 80, { r: 1, g: 0.4, b: 0 }),
|
|
100
|
+
sparks: (x: number, y: number, sys: ParticleSystem) =>
|
|
101
|
+
sys.emit(x, y, 20, { r: 1, g: 0.9, b: 0.2, size: 2, maxLife: 0.8 }),
|
|
102
|
+
smoke: (x: number, y: number, sys: ParticleSystem) =>
|
|
103
|
+
sys.emit(x, y, 10, { r: 0.5, g: 0.5, b: 0.5, size: 12, maxLife: 3, vx: 0, vy: -30 }),
|
|
104
|
+
};
|
|
105
|
+
```
|