sandhill-road
Version:
A narrative-driven startup simulation game where you guide a founder from garage to exit
351 lines • 14.4 kB
JavaScript
;
// Narrative Engine for Sandhill Road
// Handles events, choices, and outcomes
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.progressToNextStage = exports.getStageDescription = exports.clearCurrentEvent = exports.makeChoice = exports.startEvent = exports.getCurrentEvent = exports.selectRandomEvent = exports.getAvailableEvents = exports.loadEvents = void 0;
const gameState_1 = require("./gameState");
const revenueUtils_1 = require("../utils/revenueUtils");
// Event database
let eventsDatabase = [];
// Current event being shown to the player
let currentEvent = null;
// Load all events from JSON
const loadEvents = async () => {
try {
// In a browser environment, we'd fetch from a URL
// In Node, we could use fs module
let eventsData;
let moreEventsData;
if (typeof window !== 'undefined') {
// Browser environment
const response = await fetch('/data/events.json');
eventsData = await response.json();
const moreResponse = await fetch('/data/more-events.json');
moreEventsData = await moreResponse.json();
}
else {
// Node environment - using dynamic import instead of require
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
const path = await Promise.resolve().then(() => __importStar(require('path')));
const filePath = path.join(process.cwd(), 'src/data/events.json');
const fileData = await fs.readFile(filePath, 'utf-8');
eventsData = JSON.parse(fileData);
const moreFilePath = path.join(process.cwd(), 'src/data/more-events.json');
const moreFileData = await fs.readFile(moreFilePath, 'utf-8');
moreEventsData = JSON.parse(moreFileData);
}
// Combine both event arrays
eventsDatabase = [...eventsData, ...moreEventsData];
return;
}
catch (error) {
console.error("Failed to load events:", error);
throw new Error("Failed to load game events");
}
};
exports.loadEvents = loadEvents;
// Filter events based on current game state and requirements
const getAvailableEvents = () => {
const state = (0, gameState_1.getGameState)();
const currentStage = state.stageProgress.currentStage;
const completedEvents = state.stageProgress.completedEvents;
const completedExclusiveGroups = state.stageProgress.completedExclusiveGroups;
return eventsDatabase.filter(event => {
// Check if event is for the current stage
const validStage = Array.isArray(event.stage)
? event.stage.includes(currentStage)
: event.stage === currentStage;
if (!validStage)
return false;
// Check if event was already completed and is not repeatable
if (!event.repeatable && completedEvents.includes(event.id)) {
return false;
}
// Check if event's exclusive group has already been completed
if (event.exclusiveGroup && completedExclusiveGroups.includes(event.exclusiveGroup)) {
return false;
}
// Check requirements
if (event.requirements) {
for (const [stat, value] of Object.entries(event.requirements)) {
// Check if the requirement is met
const statPath = stat.split('.');
let currentValue = state;
for (const path of statPath) {
if (currentValue && currentValue[path] !== undefined) {
currentValue = currentValue[path];
}
else {
return false; // Required stat not found
}
}
// Compare the value based on type
if (typeof currentValue === 'number' && typeof value === 'number') {
if (currentValue < value) {
return false; // Requirement not met
}
}
else if (typeof currentValue === 'boolean' && typeof value === 'boolean') {
if (currentValue !== value) {
return false; // Requirement not met
}
}
}
}
return true;
});
};
exports.getAvailableEvents = getAvailableEvents;
// Select a random event from available ones, weighted by their weights
const selectRandomEvent = () => {
const availableEvents = (0, exports.getAvailableEvents)();
if (availableEvents.length === 0) {
return null;
}
// Calculate total weight
const totalWeight = availableEvents.reduce((sum, event) => sum + (event.weight || 1), 0);
// Select random event based on weight
let randomValue = Math.random() * totalWeight;
for (const event of availableEvents) {
const weight = event.weight || 1;
if (randomValue <= weight) {
return event;
}
randomValue -= weight;
}
// Fallback in case of rounding errors
return availableEvents[0];
};
exports.selectRandomEvent = selectRandomEvent;
// Get the current event
const getCurrentEvent = () => {
return currentEvent;
};
exports.getCurrentEvent = getCurrentEvent;
// Start a new event
const startEvent = (eventId) => {
let event = null;
if (eventId) {
// Find specific event
event = eventsDatabase.find(e => e.id === eventId) || null;
}
else {
// Pick a random event
event = (0, exports.selectRandomEvent)();
}
if (!event) {
return null;
}
currentEvent = event;
return event;
};
exports.startEvent = startEvent;
// Make a choice and handle the outcome
const makeChoice = (choiceId) => {
if (!currentEvent) {
return {
success: false,
resultText: "No active event."
};
}
const choice = currentEvent.choices.find(c => c.id === choiceId);
if (!choice) {
return {
success: false,
resultText: "Invalid choice."
};
}
// Check if the choice has requirements
if (choice.requires) {
const state = (0, gameState_1.getGameState)();
for (const [stat, value] of Object.entries(choice.requires)) {
// Check if the requirement is met
const statPath = stat.split('.');
let currentValue = state;
for (const path of statPath) {
if (currentValue && currentValue[path] !== undefined) {
currentValue = currentValue[path];
}
else {
return {
success: false,
resultText: "You don't meet the requirements for this choice."
};
}
}
// Compare the value based on type
if (typeof currentValue === 'number' && typeof value === 'number') {
if (currentValue < value) {
return {
success: false,
resultText: `Your ${stat} (${currentValue}) is too low. Need at least ${value}.`
};
}
}
else if (typeof currentValue === 'boolean' && typeof value === 'boolean') {
if (currentValue !== value) {
return {
success: false,
resultText: `Requirement not met: ${stat} must be ${value}.`
};
}
}
}
}
// Apply the results of the choice
(0, gameState_1.updateGameState)(state => {
// Create a copy to modify
let newState = { ...state };
// Mark the event as completed
if (currentEvent) {
newState.stageProgress.completedEvents = [
...newState.stageProgress.completedEvents,
currentEvent.id
];
if (currentEvent.exclusiveGroup) {
newState.stageProgress.completedExclusiveGroups = [
...newState.stageProgress.completedExclusiveGroups,
currentEvent.exclusiveGroup
];
}
}
// Apply the choice results
for (const [stat, value] of Object.entries(choice.result)) {
// Handle special cases
if (stat === 'weeksLost' && typeof value === 'number') {
// Add multiple weeks to the counter
for (let i = 0; i < value; i++) {
(0, gameState_1.advanceWeek)();
}
continue;
}
if (stat === 'gameOver' && typeof value === 'boolean' && value === true) {
newState.gameOver = true;
continue;
}
if (stat === 'gameOverReason' && typeof value === 'string') {
newState.gameOverReason = value;
continue;
}
// Handle nested stats (including companyFlags)
const statPath = stat.split('.');
if (statPath.length === 2) {
const [category, attribute] = statPath;
if (newState[category] &&
typeof newState[category] === 'object') {
const categoryObj = newState[category];
if (categoryObj && categoryObj[attribute] !== undefined) {
if (typeof categoryObj[attribute] === 'number' && typeof value === 'number') {
if (category === 'companyStats' && attribute === 'users' && value > 0) {
const currentUsers = categoryObj[attribute];
const newUsers = Math.max(0, currentUsers + value);
categoryObj[attribute] = newUsers;
const newRevenue = (0, revenueUtils_1.calculateRevenueFromUsers)(newUsers);
categoryObj['revenue'] = newRevenue;
}
else {
categoryObj[attribute] = Math.max(0, categoryObj[attribute] + value);
}
}
else {
// For booleans, strings, and direct number assignments
categoryObj[attribute] = value;
}
}
}
}
}
return newState;
});
// Advance time by one week unless the choice specified a different time span
if (!choice.result.weeksLost) {
(0, gameState_1.advanceWeek)();
}
// Return the result and the next event if specified
return {
success: true,
resultText: choice.resultText,
nextEvent: choice.nextEvent
};
};
exports.makeChoice = makeChoice;
// Clear the current event
const clearCurrentEvent = () => {
currentEvent = null;
};
exports.clearCurrentEvent = clearCurrentEvent;
// Helper functions for the narrative engine
const getStageDescription = (stage) => {
const descriptions = {
[gameState_1.GameStage.Garage]: "You're working from your garage, trying to build an MVP.",
[gameState_1.GameStage.DemoDay]: "It's time to present your startup at a demo day.",
[gameState_1.GameStage.Fundraising]: "You're meeting with investors to raise your seed round.",
[gameState_1.GameStage.PMF]: "You're iterating on your product to find product-market fit.",
[gameState_1.GameStage.Scaling]: "Your product is gaining traction, and it's time to scale.",
[gameState_1.GameStage.Crisis]: "Your startup is facing a major crisis.",
[gameState_1.GameStage.Exit]: "You're exploring exit opportunities for your startup."
};
return descriptions[stage] || "Unknown stage";
};
exports.getStageDescription = getStageDescription;
// Progress to the next stage
const progressToNextStage = () => {
const stageOrder = [
gameState_1.GameStage.Garage,
gameState_1.GameStage.DemoDay,
gameState_1.GameStage.Fundraising,
gameState_1.GameStage.PMF,
gameState_1.GameStage.Scaling,
gameState_1.GameStage.Crisis,
gameState_1.GameStage.Exit
];
const state = (0, gameState_1.getGameState)();
const currentStageIndex = stageOrder.indexOf(state.stageProgress.currentStage);
if (currentStageIndex < 0 || currentStageIndex >= stageOrder.length - 1) {
return state.stageProgress.currentStage;
}
const nextStage = stageOrder[currentStageIndex + 1];
(0, gameState_1.updateGameState)(state => ({
...state,
stageProgress: {
...state.stageProgress,
currentStage: nextStage
}
}));
return nextStage;
};
exports.progressToNextStage = progressToNextStage;
//# sourceMappingURL=narrativeEngine.js.map