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/gamestate.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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 };
|