pre-mortem 0.1.5 → 0.1.7

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/src/game.ts DELETED
@@ -1,366 +0,0 @@
1
- import Matter from 'matter-js';
2
- import { createBlock, BlockType } from './blocks';
3
- import { GameState, StageConfig } from './gamestate';
4
-
5
- export interface GameConfig {
6
- logoUrl?: string; // Optional logo URL
7
- width?: number;
8
- height?: number;
9
- initialRunway?: number;
10
- stages?: StageConfig[];
11
- onCrisis?: (shout: string, desc: string) => void;
12
- chaosInterval?: number; // in ms
13
- }
14
-
15
- export class PreMortemGame {
16
- private engine: Matter.Engine;
17
- private render: Matter.Render;
18
- private runner: Matter.Runner;
19
- private container: HTMLElement;
20
- private width: number;
21
- private height: number;
22
- private mouseConstraint!: Matter.MouseConstraint;
23
- private currentQuote: { text: string, author: string } | null = null;
24
- private chaosTimer: number = 10000; // Debug: Initial event after 10s
25
- private chaosInterval: number = 90000; // Default 1.5 mins
26
- private onCrisis?: (shout: string, desc: string) => void;
27
-
28
- public gameState: GameState;
29
- public spawnVertical: boolean = false;
30
-
31
- constructor(container: HTMLElement, config: GameConfig = {}) {
32
- this.container = container;
33
- this.width = config.width || container.clientWidth || 800;
34
- this.height = config.height || container.clientHeight || 600;
35
- this.onCrisis = config.onCrisis;
36
- this.chaosInterval = config.chaosInterval || 90000; // 1.5 min default
37
- // Set first timer to 10s for debug as requested, or use config if provided
38
- this.chaosTimer = config.chaosInterval ? config.chaosInterval : 10000;
39
-
40
- const defaultStages: StageConfig[] = [
41
- { name: 'Seed Round', threshold: 0, burnRate: 2_000 },
42
- { name: 'Series A', threshold: 100_000_000, burnRate: 10_000, fundingBonus: 250_000 },
43
- { name: 'Series B', threshold: 250_000_000, burnRate: 25_000, fundingBonus: 500_000 },
44
- { name: 'Series C', threshold: 500_000_000, burnRate: 50_000, fundingBonus: 1_000_000 },
45
- { name: 'Series D', threshold: 700_000_000, burnRate: 75_000, fundingBonus: 2_000_000 },
46
- { name: 'Series E', threshold: 850_000_000, burnRate: 100_000, fundingBonus: 5_000_000 },
47
- { name: 'Unicorn Status', threshold: 1_000_000_000, burnRate: 150_000, fundingBonus: 10_000_000 },
48
- ];
49
-
50
- this.gameState = new GameState({
51
- initialRunway: config.initialRunway || 200_000,
52
- stages: config.stages || defaultStages
53
- });
54
-
55
- this.engine = Matter.Engine.create();
56
- this.render = Matter.Render.create({
57
- element: this.container,
58
- engine: this.engine,
59
- options: {
60
- width: this.width,
61
- height: this.height,
62
- wireframes: false,
63
- background: '#1a1a1a',
64
- },
65
- });
66
-
67
- this.runner = Matter.Runner.create();
68
-
69
- this.initWorld();
70
- this.initMouse();
71
- this.initRenderLoop();
72
- this.initGameLoop();
73
- this.initControls();
74
-
75
- this.gameState.subscribe(() => {
76
- if (this.gameState.status === 'paused') {
77
- this.runner.enabled = false;
78
- } else if (this.gameState.status === 'playing') {
79
- this.runner.enabled = true;
80
- }
81
- });
82
- }
83
-
84
- private initControls() {
85
- window.addEventListener('keydown', (e) => {
86
- if ((e.key === 'r' || e.key === 'R') && this.gameState.isPlaying()) {
87
- this.rotateHeldBody();
88
- }
89
- });
90
- }
91
-
92
- private rotateHeldBody() {
93
- const body = this.mouseConstraint.body;
94
- if (body) {
95
- Matter.Body.rotate(body, 45 * (Math.PI / 180));
96
- }
97
- }
98
-
99
- private initWorld() {
100
- const { width, height } = this;
101
- const groundHeight = 60;
102
-
103
- const ground = Matter.Bodies.rectangle(width / 2, height - groundHeight / 2, width, groundHeight, {
104
- isStatic: true,
105
- render: { fillStyle: '#333' }
106
- });
107
-
108
- Matter.Composite.add(this.engine.world, [ground]);
109
- }
110
-
111
- private initMouse() {
112
- const mouse = Matter.Mouse.create(this.render.canvas);
113
- this.mouseConstraint = Matter.MouseConstraint.create(this.engine, {
114
- mouse: mouse,
115
- constraint: {
116
- stiffness: 0.2,
117
- render: { visible: false }
118
- }
119
- });
120
-
121
- Matter.Composite.add(this.engine.world, this.mouseConstraint);
122
- this.render.mouse = mouse;
123
- }
124
-
125
- public start() {
126
- Matter.Render.run(this.render);
127
- Matter.Runner.run(this.runner, this.engine);
128
- }
129
-
130
- public stop() {
131
- Matter.Render.stop(this.render);
132
- Matter.Runner.stop(this.runner);
133
- }
134
-
135
- private initRenderLoop() {
136
- Matter.Events.on(this.render, 'afterRender', () => {
137
- const context = this.render.context;
138
- const cx = this.width / 2;
139
- const officeWidth = 400;
140
- const dangerWidth = 700;
141
- const topOffset = 100;
142
-
143
- const officeRect = { x: cx - officeWidth/2, y: topOffset, w: officeWidth, h: this.height - topOffset };
144
- const dangerRect = { x: cx - dangerWidth/2, y: topOffset - 50, w: dangerWidth, h: this.height - topOffset + 100 };
145
-
146
- // Office Zone
147
- context.beginPath();
148
- context.setLineDash([10, 10]);
149
- context.lineWidth = 2;
150
- context.strokeStyle = 'rgba(255, 255, 255, 0.3)';
151
- context.strokeRect(officeRect.x, officeRect.y, officeRect.w, officeRect.h);
152
-
153
- context.font = '10px "Inter", sans-serif';
154
- context.fillStyle = 'rgba(255, 255, 255, 0.3)';
155
- context.textAlign = 'center';
156
- context.fillText("ACCURATE VALUATION ZONE", cx, officeRect.y - 15);
157
-
158
- // Quote
159
- if (!this.currentQuote) {
160
- const quotes = [
161
- { text: "WE ARE A\nFAMILY", author: "- CEO" },
162
- { text: "CHANGE THE\nWORLD", author: "- Founder" },
163
- { text: "DISRUPT\nEVERYTHING", author: "- VC" },
164
- { text: "MOVE FAST\nBREAK THINGS", author: "- Tech Lead" },
165
- { text: "RADICAL\nCANDOR", author: "- HR" },
166
- { text: "UNLIMITED\nPTO", author: "- Recruiter" },
167
- { text: "VISIONARY\nLEADER", author: "- LinkedIn" },
168
- { text: "10X\nGROWTH", author: "- Investor" },
169
- { text: "PIVOT\nTO AI", author: "- Board" }
170
- ];
171
- this.currentQuote = quotes[Math.floor(Math.random() * quotes.length)];
172
- }
173
-
174
- context.save();
175
- context.translate(cx, officeRect.y + officeRect.h / 3);
176
- context.rotate(-Math.PI / 12);
177
- context.textAlign = 'center';
178
- context.textBaseline = 'middle';
179
- context.font = '900 48px "Inter", sans-serif';
180
- context.fillStyle = 'rgba(255, 255, 255, 0.05)';
181
-
182
- const lines = this.currentQuote.text.split('\n');
183
- lines.forEach((line, i) => context.fillText(line, 0, i * 50));
184
-
185
- context.font = 'italic 700 24px "Inter", sans-serif';
186
- context.fillStyle = 'rgba(255, 255, 255, 0.04)';
187
- context.fillText(this.currentQuote.author, 0, lines.length * 50 + 10);
188
- context.restore();
189
-
190
- // Danger Zone
191
- context.beginPath();
192
- context.setLineDash([]);
193
- context.lineWidth = 1;
194
- context.strokeStyle = 'rgba(255, 50, 50, 0.2)';
195
- context.strokeRect(dangerRect.x, dangerRect.y, dangerRect.w, dangerRect.h);
196
-
197
- // Block Labels
198
- context.font = 'bold 13px "Inter", sans-serif';
199
- context.textAlign = 'center';
200
- context.textBaseline = 'middle';
201
- context.fillStyle = '#FFFFFF';
202
-
203
- this.engine.world.bodies.forEach((body) => {
204
- if (!body.label || body.label.includes('Body')) return;
205
- if (body.isStatic) return;
206
-
207
- const data = (body as any).gameData;
208
- const isVested = data && data.vested;
209
-
210
- context.save();
211
- context.translate(body.position.x, body.position.y);
212
- context.rotate(body.angle);
213
-
214
- if (isVested) {
215
- context.shadowColor = '#00ff7f';
216
- context.shadowBlur = 10;
217
- context.fillStyle = '#00ff7f';
218
- context.fillText(body.label + " ✓", 0, 0);
219
- } else {
220
- context.shadowColor = 'black';
221
- context.shadowBlur = 4;
222
- context.fillStyle = '#FFFFFF';
223
- context.fillText(body.label, 0, 0);
224
- }
225
-
226
- context.restore();
227
- });
228
- });
229
- }
230
-
231
- private initGameLoop() {
232
- Matter.Events.on(this.runner, 'afterUpdate', () => {
233
- if (!this.gameState.isPlaying()) return;
234
-
235
- const dt = this.runner.delta;
236
- this.gameState.tick(dt / 1000);
237
-
238
- // Chaos Timer
239
- if (this.gameState.valuation >= 1_000_000) {
240
- this.chaosTimer -= dt;
241
- if (this.chaosTimer <= 0) {
242
- this.triggerChaosEvent();
243
- this.chaosTimer = this.chaosInterval;
244
- }
245
- }
246
-
247
- const cx = this.width / 2;
248
- const officeWidth = 400;
249
- const dangerWidth = 700;
250
-
251
- this.engine.world.bodies.forEach(body => {
252
- if (body.isStatic) return;
253
- const data = (body as any).gameData;
254
-
255
- const isOutside = Math.abs(body.position.x - cx) > dangerWidth / 2 ||
256
- body.position.y > this.height + 50;
257
-
258
- if (isOutside) {
259
- Matter.Composite.remove(this.engine.world, body);
260
- const reasons = ["VC Scolding!", "Server Outage!", "GDPR Violation!", "TechCrunch Hit Piece!", "IP Lawsuit!"];
261
- this.gameState.penalty(50_000, reasons[Math.floor(Math.random() * reasons.length)]);
262
- return;
263
- }
264
-
265
- if (data && !data.vested) {
266
- const IsStill = body.speed < 0.15 && Math.abs(body.angularVelocity) < 0.05;
267
- const Inside = Math.abs(body.position.x - cx) < officeWidth / 2;
268
-
269
- if (IsStill && Inside) {
270
- data.vestTimer += this.runner.delta;
271
- if (data.vestTimer > 1000) {
272
- data.vested = true;
273
- this.gameState.addValuation(data.value);
274
- }
275
- } else {
276
- data.vestTimer = 0;
277
- }
278
- }
279
- });
280
- });
281
- }
282
-
283
- private triggerChaosEvent() {
284
- const eventId = Math.floor(Math.random() * 5);
285
- switch (eventId) {
286
- case 0:
287
- this.onCrisis?.("STRATEGIC PIVOT", "CEO: 'We are pivoting to AI!' (Gravity Shift)");
288
- this.engine.world.gravity.x = 0.2; // Reduced from 0.5 as requested
289
- setTimeout(() => { this.engine.world.gravity.x = 0; }, 5000);
290
- break;
291
- case 1:
292
- this.onCrisis?.("THE BIG REORG", "HR: 'New Org Chart Announced!' (No Friction)");
293
- this.engine.world.bodies.forEach(b => {
294
- if (b.isStatic) return;
295
- (b as any).oldFriction = b.friction;
296
- b.friction = 0;
297
- b.frictionStatic = 0;
298
- });
299
- setTimeout(() => {
300
- this.engine.world.bodies.forEach(b => {
301
- if (b.isStatic) return;
302
- b.friction = (b as any).oldFriction || 0.1;
303
- b.frictionStatic = 0.5;
304
- });
305
- }, 5000);
306
- break;
307
- case 2:
308
- this.onCrisis?.("LEGACY DEPRECATION", "CTO: 'Deprecating v1 API!' (Ghost Blocks)");
309
- const active = this.engine.world.bodies.filter(b => !b.isStatic);
310
- if (active.length > 3) {
311
- const target = active[Math.floor(Math.random() * active.length)];
312
- target.isSensor = true;
313
- setTimeout(() => { target.isSensor = false; }, 4000);
314
- }
315
- break;
316
- case 3:
317
- this.onCrisis?.("FEATURE CREEP", "PM: 'Just one more feature...' (1.2x Scaling)");
318
- this.engine.world.bodies.forEach(b => {
319
- if (!b.isStatic) Matter.Body.scale(b, 1.2, 1.2);
320
- });
321
- break;
322
- case 4:
323
- this.onCrisis?.("VIRAL SPIKE", "DevOps: 'Traffic Spike!' (Earthquake)");
324
- let shakes = 0;
325
- const interval = setInterval(() => {
326
- this.engine.world.bodies.forEach(b => {
327
- if (!b.isStatic) Matter.Body.applyForce(b, b.position, { x: (Math.random()-0.5)*0.05, y: (Math.random()-0.5)*0.05 });
328
- });
329
- if (++shakes > 20) clearInterval(interval);
330
- }, 100);
331
- break;
332
- }
333
- }
334
-
335
- public toggleRotation() {
336
- this.spawnVertical = !this.spawnVertical;
337
- }
338
-
339
- public spawnBlock(type: BlockType) {
340
- if (!this.gameState.isPlaying()) return;
341
- if (Math.random() < 1/15) {
342
- const toxics = [BlockType.SLACKER, BlockType.CONTROL_FREAK, BlockType.PRIMA_DONNA, BlockType.QUIET_QUITTER, BlockType.SLACK_SPAMMER];
343
- type = toxics[Math.floor(Math.random() * toxics.length)];
344
- }
345
-
346
- if (type === BlockType.BOOTSTRAP) this.gameState.spendMoney(5_000);
347
- else if (type === BlockType.VC) this.gameState.addMoney(75_000); // 1.5x of 50k
348
-
349
- const x = this.width / 2 + (Math.random() - 0.5) * 40;
350
- const block = createBlock(x, y, type); // y is 100
351
- let angle = this.spawnVertical ? 90 * (Math.PI / 180) : 0;
352
- if (type === BlockType.VC) angle += (Math.random() - 0.5) * (30 * (Math.PI / 180));
353
- Matter.Body.setAngle(block, angle);
354
-
355
- (block as any).gameData = {
356
- type,
357
- value: type === BlockType.VC ? 50_000_000 : (type === BlockType.BOOTSTRAP ? 5_000_000 : 0),
358
- vested: false,
359
- vestTimer: 0
360
- };
361
- Matter.Composite.add(this.engine.world, block);
362
- }
363
- }
364
-
365
- // Fixed constant y at the end
366
- const y = 100;
package/src/gamestate.ts DELETED
@@ -1,176 +0,0 @@
1
- export interface StageConfig {
2
- name: string;
3
- threshold: number; // Valuation required to reach this stage
4
- burnRate: number; // Dollars per second
5
- fundingBonus?: number; // Cash awarded when reaching this stage
6
- }
7
-
8
- export interface GameStateConfig {
9
- initialRunway: number;
10
- stages: StageConfig[];
11
- }
12
-
13
- export enum GameStatus {
14
- PLAYING = 'playing',
15
- PAUSED = 'paused',
16
- BANKRUPT = 'bankrupt',
17
- COLLAPSED = 'collapsed',
18
- WIN = 'exited',
19
- }
20
-
21
- export class GameState {
22
- public runway: number;
23
- public valuation: number = 0;
24
- public stage: string; // Dynamic name
25
- public status: GameStatus = GameStatus.PLAYING;
26
- public lastPenaltyReason: string = '';
27
-
28
- private config: GameStateConfig;
29
- private currentStageIndex: number = 0;
30
- private listeners: ((event?: string) => void)[] = [];
31
-
32
- constructor(config: GameStateConfig) {
33
- this.config = config;
34
- this.runway = config.initialRunway;
35
-
36
- // Validate stages
37
- if (!this.config.stages || this.config.stages.length === 0) {
38
- throw new Error("Game must have at least one stage defined.");
39
- }
40
-
41
- // Sort stages by threshold to be safe
42
- this.config.stages.sort((a, b) => a.threshold - b.threshold);
43
-
44
- this.currentStageIndex = 0;
45
- this.stage = this.config.stages[0].name;
46
- this.checkStage();
47
- }
48
-
49
- public setPaused(paused: boolean) {
50
- if (paused) {
51
- if (this.status === GameStatus.PLAYING) this.status = GameStatus.PAUSED;
52
- } else {
53
- if (this.status === GameStatus.PAUSED) this.status = GameStatus.PLAYING;
54
- }
55
- this.notify();
56
- }
57
-
58
- public togglePause() {
59
- this.setPaused(this.status === GameStatus.PLAYING);
60
- }
61
-
62
- public tick(dt: number) {
63
- if (this.status !== GameStatus.PLAYING) return;
64
-
65
- const safeDt = Math.min(dt, 0.1);
66
- const currentBurnRate = this.getDynamicBurnRate();
67
-
68
- this.runway -= currentBurnRate * safeDt;
69
-
70
- if (this.runway <= 0) {
71
- this.runway = 0;
72
- this.status = GameStatus.BANKRUPT;
73
- }
74
-
75
- // Check win condition (Final Stage Reached?)
76
- // Actually, traditionally Unicorn Jenga ends at Unicorn.
77
- // Let's assume the LAST stage is the goal? Or specific logic?
78
- // User said "Next is where...", implies progression.
79
- // If we exceed the highest threshold, we win?
80
-
81
- const finalStage = this.config.stages[this.config.stages.length - 1];
82
- if (this.valuation >= finalStage.threshold && this.status === GameStatus.PLAYING) {
83
- this.status = GameStatus.WIN;
84
- }
85
-
86
- this.checkStage();
87
- this.notify();
88
- }
89
-
90
- private getDynamicBurnRate(): number {
91
- return this.config.stages[this.currentStageIndex].burnRate;
92
- }
93
-
94
- public addValuation(amount: number) {
95
- this.valuation += amount;
96
- this.checkStage();
97
- this.notify();
98
- }
99
-
100
- public spendMoney(amount: number) {
101
- this.runway -= amount;
102
- this.notify();
103
- }
104
-
105
- public addMoney(amount: number) {
106
- this.runway += amount;
107
- this.notify();
108
- }
109
-
110
- public setCollapsed() {
111
- if (this.status !== GameStatus.PLAYING) return;
112
- this.status = GameStatus.COLLAPSED;
113
- this.notify();
114
- }
115
-
116
- public getNextStageThreshold(): number {
117
- // Return the threshold of the NEXT stage
118
- if (this.currentStageIndex < this.config.stages.length - 1) {
119
- return this.config.stages[this.currentStageIndex + 1].threshold;
120
- }
121
- // If at last stage, goal is met? Or maybe return the current threshold?
122
- return this.config.stages[this.config.stages.length - 1].threshold;
123
- }
124
-
125
- public penalty(amount: number, reason: string) {
126
- this.runway -= amount;
127
- this.lastPenaltyReason = reason;
128
- if (this.runway < 0) {
129
- this.runway = 0;
130
- this.status = GameStatus.BANKRUPT;
131
- }
132
- this.notify('penalty');
133
- }
134
-
135
- private checkStage() {
136
- // Determine stage based on valuation
137
- // Find highest threshold we have crossed
138
- let newIndex = 0;
139
- for (let i = 0; i < this.config.stages.length; i++) {
140
- if (this.valuation >= this.config.stages[i].threshold) {
141
- newIndex = i;
142
- } else {
143
- break; // Since we sorted, we can stop
144
- }
145
- }
146
-
147
- if (newIndex !== this.currentStageIndex) {
148
- // Award bonus if we progressed forward
149
- if (newIndex > this.currentStageIndex) {
150
- const bonus = this.config.stages[newIndex].fundingBonus || 0;
151
- if (bonus > 0) {
152
- this.addMoney(bonus);
153
- this.notify('funding:' + bonus);
154
- }
155
- }
156
- this.currentStageIndex = newIndex;
157
- this.stage = this.config.stages[newIndex].name;
158
- }
159
- }
160
-
161
- public subscribe(listener: (event?: string) => void) {
162
- this.listeners.push(listener);
163
- }
164
-
165
- private notify(event?: string) {
166
- this.listeners.forEach((listener) => listener(event));
167
- }
168
-
169
- public isPlaying(): boolean {
170
- return this.status === GameStatus.PLAYING;
171
- }
172
-
173
- public isBankrupt(): boolean {
174
- return this.status === GameStatus.BANKRUPT;
175
- }
176
- }
package/src/index.ts DELETED
@@ -1,30 +0,0 @@
1
- import { PreMortemGame, GameConfig } from './game';
2
- import { UIManager } from './ui';
3
-
4
- export class PreMortem {
5
- private game: PreMortemGame;
6
- private ui: UIManager;
7
-
8
- constructor(container: HTMLElement, config: GameConfig = {}) {
9
- this.game = new PreMortemGame(container, {
10
- ...config,
11
- onCrisis: (shout, desc) => this.ui.showCrisisAlert(shout, desc)
12
- });
13
- this.ui = new UIManager(container, {
14
- onSpawn: (type) => this.game.spawnBlock(type),
15
- onRotate: () => this.game.toggleRotation(),
16
- gameState: this.game.gameState,
17
- });
18
-
19
- if (config.logoUrl) {
20
- this.ui.showLogo(config.logoUrl);
21
- }
22
- }
23
-
24
- public start() {
25
- this.game.start();
26
- }
27
- }
28
-
29
- // Export parts if needed directly
30
- export { PreMortemGame, UIManager };