UNPKG

sandhill-road

Version:

A narrative-driven startup simulation game where you guide a founder from garage to exit

351 lines 14.4 kB
"use strict"; // 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