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/dist/pre-mortem.gif +0 -0
- package/dist/pre-mortem.mjs +45 -46
- package/dist/pre-mortem.umd.js +8 -8
- package/package.json +6 -1
- package/index.html +0 -39
- package/public/control-freak.png +0 -0
- package/public/logo.png +0 -0
- package/public/pre-mortem-icon.png +0 -0
- package/public/pre-mortem.gif +0 -0
- package/public/pre-mortem.mp4 +0 -0
- package/public/prima-donna.png +0 -0
- package/public/quet-quitter.png +0 -0
- package/public/slack-spammer.png +0 -0
- package/public/slacker.png +0 -0
- package/src/blocks.ts +0 -182
- package/src/game.ts +0 -366
- package/src/gamestate.ts +0 -176
- package/src/index.ts +0 -30
- package/src/ui.ts +0 -634
- package/tsconfig.json +0 -21
- package/vite.config.ts +0 -20
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 };
|