pre-mortem 0.1.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 -0
- package/README.md +86 -0
- package/dist/control-freak.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/pre-mortem-icon.png +0 -0
- package/dist/pre-mortem.mjs +1005 -0
- package/dist/pre-mortem.mp4 +0 -0
- package/dist/pre-mortem.umd.js +382 -0
- package/dist/prima-donna.png +0 -0
- package/dist/quet-quitter.png +0 -0
- package/dist/slack-spammer.png +0 -0
- package/dist/slacker.png +0 -0
- package/index.html +39 -0
- package/package.json +30 -0
- 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.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 +182 -0
- package/src/game.ts +366 -0
- package/src/gamestate.ts +176 -0
- package/src/index.ts +30 -0
- package/src/ui.ts +634 -0
- package/tsconfig.json +21 -0
- package/vite.config.ts +20 -0
package/src/blocks.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import Matter from 'matter-js';
|
|
2
|
+
|
|
3
|
+
export enum BlockType {
|
|
4
|
+
BOOTSTRAP = 'bootstrap',
|
|
5
|
+
VC = 'vc',
|
|
6
|
+
SLACKER = 'slacker',
|
|
7
|
+
CONTROL_FREAK = 'control_freak',
|
|
8
|
+
PRIMA_DONNA = 'prima_donna',
|
|
9
|
+
QUIET_QUITTER = 'quiet_quitter',
|
|
10
|
+
SLACK_SPAMMER = 'slack_spammer'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BlockConfig {
|
|
14
|
+
w: number;
|
|
15
|
+
h: number;
|
|
16
|
+
color: string;
|
|
17
|
+
density: number;
|
|
18
|
+
friction: number;
|
|
19
|
+
restitution: number;
|
|
20
|
+
label: string;
|
|
21
|
+
possibleLabels?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const BLOCK_DEFINITIONS: Record<BlockType, BlockConfig> = {
|
|
25
|
+
[BlockType.BOOTSTRAP]: {
|
|
26
|
+
w: 132,
|
|
27
|
+
h: 44,
|
|
28
|
+
color: '#555555', // Concrete/Rust
|
|
29
|
+
density: 0.005, // High relative density (Matter.js default is 0.001)
|
|
30
|
+
friction: 0.8,
|
|
31
|
+
restitution: 0.0,
|
|
32
|
+
label: 'Refactoring',
|
|
33
|
+
possibleLabels: [
|
|
34
|
+
// The Work (Old & New)
|
|
35
|
+
'Refactoring', 'Unit Tests', 'Bug Fixes', 'User Support',
|
|
36
|
+
'Documentation', 'Security Audit', 'Compliance', 'Cleanup',
|
|
37
|
+
'Optimization', 'CI/CD Pipeline', 'Code Review', 'DB Tuning',
|
|
38
|
+
'API Design', 'Accessibility', 'Localization', 'Error Logs',
|
|
39
|
+
'Backups', 'Legacy Code', 'Hiring', 'Customer Love',
|
|
40
|
+
'Bossware', 'YAML Hell', 'DNS Propagation', 'Regex', 'npm audit',
|
|
41
|
+
'GDPR / SOC2', 'Migration', 'Certificate Expiry', 'On-Call',
|
|
42
|
+
'Technical Debt', 'Works on my machine', 'Yak Shaving'
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
[BlockType.VC]: {
|
|
46
|
+
w: 120,
|
|
47
|
+
h: 40,
|
|
48
|
+
color: '#FF00FF', // Neon Pink
|
|
49
|
+
density: 0.0005, // Slightly heavier (was 0.0001)
|
|
50
|
+
friction: 0.3, // Less slippery (was 0.1)
|
|
51
|
+
restitution: 0.6, // Less bouncy (was 0.9)
|
|
52
|
+
label: 'AI Blockchain',
|
|
53
|
+
possibleLabels: [
|
|
54
|
+
// The Hype (Old & New)
|
|
55
|
+
'GenAI', 'Blockchain', 'Web3', 'Metaverse', 'NFTs',
|
|
56
|
+
'Viral Growth', 'Pivot', 'Synergy', 'Disruption',
|
|
57
|
+
'Thought Leader', 'Influencers', 'Hyper-Scale', 'Quantum',
|
|
58
|
+
'Big Data', 'Growth Hack', 'Paradigm Shift', '10x Engineer',
|
|
59
|
+
'Visionary', 'Series B Pitch', 'Exit Strategy',
|
|
60
|
+
'Vibe Coding', 'Founder Mode', 'Prompt Engineering', 'AGI',
|
|
61
|
+
'AI Wrapper', 'Unlimited PTO', 'Radical Candor', 'Community Led',
|
|
62
|
+
'Pre-Revenue', 'Fractional CxO'
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
[BlockType.SLACKER]: {
|
|
66
|
+
w: 64, // 80 * 0.8
|
|
67
|
+
h: 64, // 80 * 0.8
|
|
68
|
+
color: '#FFFFFF',
|
|
69
|
+
density: 0.01,
|
|
70
|
+
friction: 1.0,
|
|
71
|
+
restitution: 0.1,
|
|
72
|
+
label: 'The Slacker',
|
|
73
|
+
possibleLabels: ['The Slacker']
|
|
74
|
+
},
|
|
75
|
+
[BlockType.CONTROL_FREAK]: {
|
|
76
|
+
w: 64, // Square to match image aspect ratio
|
|
77
|
+
h: 64,
|
|
78
|
+
color: '#FF0000', density: 0.012, friction: 0.5, restitution: 0.2,
|
|
79
|
+
label: 'Control Freak', possibleLabels: ['Control Freak']
|
|
80
|
+
},
|
|
81
|
+
[BlockType.PRIMA_DONNA]: {
|
|
82
|
+
w: 56, h: 56, // 70 * 0.8
|
|
83
|
+
color: '#FFFF00', density: 0.008, friction: 0.1, restitution: 1.2,
|
|
84
|
+
label: 'Prima Donna', possibleLabels: ['Prima Donna']
|
|
85
|
+
},
|
|
86
|
+
[BlockType.QUIET_QUITTER]: {
|
|
87
|
+
w: 48, h: 48, // 60 * 0.8
|
|
88
|
+
color: '#00FFFF', density: 0.005, friction: 0.01, restitution: 0.9,
|
|
89
|
+
label: 'Quiet Quitter', possibleLabels: ['Quiet Quitter']
|
|
90
|
+
},
|
|
91
|
+
[BlockType.SLACK_SPAMMER]: {
|
|
92
|
+
w: 60, h: 60, // 75 * 0.8
|
|
93
|
+
color: '#00FF00', density: 0.01, friction: 0.4, restitution: 0.4,
|
|
94
|
+
label: 'Slack Spammer', possibleLabels: ['Slack Spammer']
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const getRandomLabel = (type: BlockType): string => {
|
|
99
|
+
const labels = BLOCK_DEFINITIONS[type].possibleLabels || [];
|
|
100
|
+
return labels[Math.floor(Math.random() * labels.length)];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const createBlock = (x: number, y: number, type: BlockType) => {
|
|
104
|
+
const def = BLOCK_DEFINITIONS[type];
|
|
105
|
+
const labelText = getRandomLabel(type);
|
|
106
|
+
|
|
107
|
+
const options = {
|
|
108
|
+
density: def.density,
|
|
109
|
+
friction: def.friction,
|
|
110
|
+
restitution: def.restitution,
|
|
111
|
+
render: {
|
|
112
|
+
fillStyle: def.color,
|
|
113
|
+
strokeStyle: '#000',
|
|
114
|
+
lineWidth: 2,
|
|
115
|
+
},
|
|
116
|
+
label: labelText,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (type === BlockType.VC) {
|
|
120
|
+
// "Long Side Trapezoid" / Distorted Wedge
|
|
121
|
+
// Create an irregular quadrilateral that is annoying to stack
|
|
122
|
+
const w = def.w;
|
|
123
|
+
const h = def.h;
|
|
124
|
+
|
|
125
|
+
// Randomize the top corners to create a slanted roof (Wedge)
|
|
126
|
+
// or skew the sides (Parallelogram)
|
|
127
|
+
const y1 = Math.random() * 15; // Top-Left drop
|
|
128
|
+
const y2 = Math.random() * 15; // Top-Right drop
|
|
129
|
+
const skew = (Math.random() - 0.5) * 20; // skew x
|
|
130
|
+
|
|
131
|
+
const vertices = [
|
|
132
|
+
{ x: 0 + skew, y: y1 }, // TL
|
|
133
|
+
{ x: w + skew, y: y2 }, // TR
|
|
134
|
+
{ x: w, y: h }, // BR
|
|
135
|
+
{ x: 0, y: h } // BL
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
return Matter.Bodies.fromVertices(x, y, [vertices], options);
|
|
139
|
+
return Matter.Bodies.fromVertices(x, y, [vertices], options);
|
|
140
|
+
} else if (type === BlockType.SLACKER ||
|
|
141
|
+
type === BlockType.CONTROL_FREAK ||
|
|
142
|
+
type === BlockType.PRIMA_DONNA ||
|
|
143
|
+
type === BlockType.QUIET_QUITTER ||
|
|
144
|
+
type === BlockType.SLACK_SPAMMER) {
|
|
145
|
+
|
|
146
|
+
const textureMap: Record<string, string> = {
|
|
147
|
+
[BlockType.SLACKER]: './slacker.png',
|
|
148
|
+
[BlockType.CONTROL_FREAK]: './control-freak.png',
|
|
149
|
+
[BlockType.PRIMA_DONNA]: './prima-donna.png',
|
|
150
|
+
[BlockType.QUIET_QUITTER]: './quet-quitter.png', // User typo kept
|
|
151
|
+
[BlockType.SLACK_SPAMMER]: './slack-spammer.png'
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const spriteOptions = {
|
|
155
|
+
...options,
|
|
156
|
+
render: {
|
|
157
|
+
sprite: {
|
|
158
|
+
texture: textureMap[type],
|
|
159
|
+
xScale: (def.w / 512) * 2, // Approximate
|
|
160
|
+
yScale: (def.h / 512) * 2
|
|
161
|
+
},
|
|
162
|
+
lineWidth: 3,
|
|
163
|
+
strokeStyle: '#ffffff'
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (type === BlockType.QUIET_QUITTER) {
|
|
168
|
+
return Matter.Bodies.circle(x, y, def.w / 2, spriteOptions);
|
|
169
|
+
} else if (type === BlockType.CONTROL_FREAK) {
|
|
170
|
+
return Matter.Bodies.trapezoid(x, y, def.w, def.h, 0.5, spriteOptions);
|
|
171
|
+
} else if (type === BlockType.PRIMA_DONNA) {
|
|
172
|
+
return Matter.Bodies.polygon(x, y, 5, def.w / 2, spriteOptions); // Pentagon
|
|
173
|
+
} else if (type === BlockType.SLACK_SPAMMER) {
|
|
174
|
+
return Matter.Bodies.polygon(x, y, 6, def.w / 2, spriteOptions); // Hexagon
|
|
175
|
+
} else {
|
|
176
|
+
// Default Slacker (Square)
|
|
177
|
+
return Matter.Bodies.rectangle(x, y, def.w, def.h, spriteOptions);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
return Matter.Bodies.rectangle(x, y, def.w, def.h, options);
|
|
181
|
+
}
|
|
182
|
+
};
|
package/src/game.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
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;
|