676 lines
26 KiB
JavaScript
676 lines
26 KiB
JavaScript
|
|
// ============================================================
|
||
|
|
// THE BEACON - Emergent Game Mechanics
|
||
|
|
// The game evolves alongside its players.
|
||
|
|
// Tracks behavior patterns, detects strategies, generates
|
||
|
|
// dynamic events that reward or challenge those strategies.
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
class EmergentMechanics {
|
||
|
|
constructor() {
|
||
|
|
this.SAVE_KEY = 'the-beacon-emergent-v1';
|
||
|
|
this.PATTERN_CHECK_INTERVAL = 30; // seconds between pattern checks
|
||
|
|
this.MIN_ACTIONS_FOR_PATTERN = 20; // minimum tracked actions before detection kicks in
|
||
|
|
this.EVENT_COOLDOWN = 120; // seconds between emergent events
|
||
|
|
this.lastPatternCheck = 0;
|
||
|
|
this.lastEventTime = 0;
|
||
|
|
|
||
|
|
// Behavior tracking buffers
|
||
|
|
this.actions = []; // [{action, data, time}]
|
||
|
|
this.clickTimestamps = []; // last N click times for frequency analysis
|
||
|
|
this.resourceDeltas = []; // [{resource, delta, time}]
|
||
|
|
this.upgradeChoices = []; // [{buildingId, time}]
|
||
|
|
this.idlePeriods = []; // [{start, duration}]
|
||
|
|
|
||
|
|
// Detected patterns with confidence scores (0-1)
|
||
|
|
this.patterns = {
|
||
|
|
hoarder: 0,
|
||
|
|
rusher: 0,
|
||
|
|
optimizer: 0,
|
||
|
|
idle_player: 0,
|
||
|
|
clicker: 0,
|
||
|
|
balanced: 0
|
||
|
|
};
|
||
|
|
|
||
|
|
// Active emergent events
|
||
|
|
this.activeEvents = [];
|
||
|
|
|
||
|
|
// History of generated events (for avoiding repetition)
|
||
|
|
this.eventHistory = [];
|
||
|
|
|
||
|
|
// Stats
|
||
|
|
this.totalPatternsDetected = 0;
|
||
|
|
this.totalEventsGenerated = 0;
|
||
|
|
this.lastIdleCheckTime = Date.now();
|
||
|
|
this.lastActionTime = Date.now();
|
||
|
|
|
||
|
|
// Load saved state
|
||
|
|
this._load();
|
||
|
|
}
|
||
|
|
|
||
|
|
// === BEHAVIOR TRACKING ===
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Track a player action. Called by game systems.
|
||
|
|
* @param {string} action - Action type: 'click', 'buy_building', 'buy_project', 'ops_convert', 'sprint', 'resolve_event'
|
||
|
|
* @param {object} data - Action-specific data
|
||
|
|
*/
|
||
|
|
track(action, data) {
|
||
|
|
const now = Date.now();
|
||
|
|
const entry = { action, data: data || {}, time: now };
|
||
|
|
this.actions.push(entry);
|
||
|
|
this.lastActionTime = now;
|
||
|
|
|
||
|
|
// Track click frequency
|
||
|
|
if (action === 'click') {
|
||
|
|
this.clickTimestamps.push(now);
|
||
|
|
// Keep only last 100 clicks for frequency analysis
|
||
|
|
if (this.clickTimestamps.length > 100) {
|
||
|
|
this.clickTimestamps.shift();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Track resource deltas
|
||
|
|
if (data && data.resource && data.delta !== undefined) {
|
||
|
|
this.resourceDeltas.push({
|
||
|
|
resource: data.resource,
|
||
|
|
delta: data.delta,
|
||
|
|
time: now
|
||
|
|
});
|
||
|
|
if (this.resourceDeltas.length > 200) {
|
||
|
|
this.resourceDeltas.shift();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Track building purchases
|
||
|
|
if (action === 'buy_building' && data && data.buildingId) {
|
||
|
|
this.upgradeChoices.push({
|
||
|
|
buildingId: data.buildingId,
|
||
|
|
time: now
|
||
|
|
});
|
||
|
|
if (this.upgradeChoices.length > 100) {
|
||
|
|
this.upgradeChoices.shift();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Trim old action history (keep last 500)
|
||
|
|
if (this.actions.length > 500) {
|
||
|
|
this.actions = this.actions.slice(-500);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Detect idle periods
|
||
|
|
this._checkIdlePeriod(now);
|
||
|
|
|
||
|
|
// Periodically detect patterns
|
||
|
|
const elapsedSec = (now - this.lastPatternCheck) / 1000;
|
||
|
|
if (elapsedSec >= this.PATTERN_CHECK_INTERVAL && this.actions.length >= this.MIN_ACTIONS_FOR_PATTERN) {
|
||
|
|
this.detectPatterns();
|
||
|
|
this.lastPatternCheck = now;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Track a resource snapshot from the game state.
|
||
|
|
* Called each tick to compare against player behavior.
|
||
|
|
*/
|
||
|
|
trackResourceSnapshot(g) {
|
||
|
|
if (!g) return;
|
||
|
|
this._lastSnapshot = {
|
||
|
|
code: g.code,
|
||
|
|
compute: g.compute,
|
||
|
|
knowledge: g.knowledge,
|
||
|
|
users: g.users,
|
||
|
|
impact: g.impact,
|
||
|
|
ops: g.ops,
|
||
|
|
trust: g.trust,
|
||
|
|
harmony: g.harmony,
|
||
|
|
phase: g.phase,
|
||
|
|
totalClicks: g.totalClicks,
|
||
|
|
playTime: g.playTime,
|
||
|
|
buildings: { ...g.buildings },
|
||
|
|
time: Date.now()
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// === PATTERN DETECTION ===
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Analyze tracked behavior to detect player strategies.
|
||
|
|
* Updates this.patterns with confidence scores (0-1).
|
||
|
|
*/
|
||
|
|
detectPatterns() {
|
||
|
|
const now = Date.now();
|
||
|
|
const snap = this._lastSnapshot;
|
||
|
|
if (!snap) return this.patterns;
|
||
|
|
|
||
|
|
// Reset low-confidence patterns to decay over time
|
||
|
|
for (const key of Object.keys(this.patterns)) {
|
||
|
|
this.patterns[key] *= 0.9;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- HOARDER: Accumulates resources without spending ---
|
||
|
|
this._detectHoarder(snap);
|
||
|
|
|
||
|
|
// --- RUSHER: Spends resources immediately, rapid building ---
|
||
|
|
this._detectRusher(snap);
|
||
|
|
|
||
|
|
// --- OPTIMIZER: Focuses on efficiency, maxes click combos ---
|
||
|
|
this._detectOptimizer(snap);
|
||
|
|
|
||
|
|
// --- IDLE PLAYER: Low click frequency, relies on passive generation ---
|
||
|
|
this._detectIdlePlayer();
|
||
|
|
|
||
|
|
// --- CLICKER: Very high click frequency ---
|
||
|
|
this._detectClicker();
|
||
|
|
|
||
|
|
// --- BALANCED: Spread across resource types and building categories ---
|
||
|
|
this._detectBalanced(snap);
|
||
|
|
|
||
|
|
// Clamp all to [0, 1]
|
||
|
|
for (const key of Object.keys(this.patterns)) {
|
||
|
|
this.patterns[key] = Math.max(0, Math.min(1, this.patterns[key]));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find dominant pattern
|
||
|
|
let dominant = null;
|
||
|
|
let dominantConf = 0;
|
||
|
|
for (const [key, conf] of Object.entries(this.patterns)) {
|
||
|
|
if (conf > dominantConf) {
|
||
|
|
dominantConf = conf;
|
||
|
|
dominant = key;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (dominant && dominantConf > 0.5) {
|
||
|
|
this.totalPatternsDetected++;
|
||
|
|
}
|
||
|
|
|
||
|
|
this._save();
|
||
|
|
return this.patterns;
|
||
|
|
}
|
||
|
|
|
||
|
|
_detectHoarder(snap) {
|
||
|
|
// High resource accumulation relative to spending
|
||
|
|
const recentPurchases = this.upgradeChoices.filter(
|
||
|
|
u => u.time > Date.now() - 120000
|
||
|
|
).length;
|
||
|
|
|
||
|
|
// Look at resource deltas: positive deltas without corresponding purchases
|
||
|
|
const recentDeltas = this.resourceDeltas.filter(
|
||
|
|
d => d.time > Date.now() - 120000 && d.delta > 0
|
||
|
|
);
|
||
|
|
const totalAccumulated = recentDeltas.reduce((sum, d) => sum + d.delta, 0);
|
||
|
|
|
||
|
|
// If accumulating a lot but not spending, it's hoarding
|
||
|
|
if (totalAccumulated > 1000 && recentPurchases < 2) {
|
||
|
|
this.patterns.hoarder = Math.min(1, this.patterns.hoarder + 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if resources are high relative to phase
|
||
|
|
const codeThresholds = [0, 500, 5000, 50000, 500000, 5000000];
|
||
|
|
const threshold = codeThresholds[Math.min(snap.phase, 5)] || 0;
|
||
|
|
if (threshold > 0 && snap.code > threshold * 3) {
|
||
|
|
this.patterns.hoarder = Math.min(1, this.patterns.hoarder + 0.1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_detectRusher(snap) {
|
||
|
|
// Rapid building purchases in a short time
|
||
|
|
const recentPurchases = this.upgradeChoices.filter(
|
||
|
|
u => u.time > Date.now() - 60000
|
||
|
|
).length;
|
||
|
|
|
||
|
|
if (recentPurchases >= 5) {
|
||
|
|
this.patterns.rusher = Math.min(1, this.patterns.rusher + 0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Resources spent faster than they're accumulated (spending ratio)
|
||
|
|
const recentSpendDeltas = this.resourceDeltas.filter(
|
||
|
|
d => d.time > Date.now() - 60000 && d.delta < 0
|
||
|
|
);
|
||
|
|
const totalSpent = Math.abs(recentSpendDeltas.reduce((sum, d) => sum + d.delta, 0));
|
||
|
|
if (totalSpent > 500) {
|
||
|
|
this.patterns.rusher = Math.min(1, this.patterns.rusher + 0.1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_detectOptimizer(snap) {
|
||
|
|
// Sustained high combo counts, efficient ops usage
|
||
|
|
if (this.clickTimestamps.length >= 20) {
|
||
|
|
const recent = this.clickTimestamps.slice(-20);
|
||
|
|
const intervals = [];
|
||
|
|
for (let i = 1; i < recent.length; i++) {
|
||
|
|
intervals.push(recent[i] - recent[i - 1]);
|
||
|
|
}
|
||
|
|
// Consistent click timing = optimized clicking
|
||
|
|
const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
||
|
|
const variance = intervals.reduce((sum, i) => sum + (i - avg) ** 2, 0) / intervals.length;
|
||
|
|
const stddev = Math.sqrt(variance);
|
||
|
|
|
||
|
|
// Low variance with fast timing = optimizer
|
||
|
|
if (avg < 500 && stddev < avg * 0.3) {
|
||
|
|
this.patterns.optimizer = Math.min(1, this.patterns.optimizer + 0.15);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Efficient ops conversion (converts at near-max ops)
|
||
|
|
const opsConverts = this.actions.filter(
|
||
|
|
a => a.action === 'ops_convert' && a.time > Date.now() - 120000
|
||
|
|
).length;
|
||
|
|
if (opsConverts >= 10) {
|
||
|
|
this.patterns.optimizer = Math.min(1, this.patterns.optimizer + 0.1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_detectIdlePlayer() {
|
||
|
|
// Long gaps between actions
|
||
|
|
const recentActions = this.actions.filter(a => a.time > Date.now() - 300000);
|
||
|
|
if (recentActions.length < 5 && this.actions.length > 10) {
|
||
|
|
this.patterns.idle_player = Math.min(1, this.patterns.idle_player + 0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Very low click frequency
|
||
|
|
const recentClicks = this.clickTimestamps.filter(t => t > Date.now() - 120000);
|
||
|
|
if (recentClicks.length < 3 && this.clickTimestamps.length > 10) {
|
||
|
|
this.patterns.idle_player = Math.min(1, this.patterns.idle_player + 0.15);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_detectClicker() {
|
||
|
|
if (this.clickTimestamps.length < 10) return;
|
||
|
|
|
||
|
|
const recent = this.clickTimestamps.filter(t => t > Date.now() - 30000);
|
||
|
|
const clicksPerSecond = recent.length / 30;
|
||
|
|
|
||
|
|
if (clicksPerSecond > 3) {
|
||
|
|
this.patterns.clicker = Math.min(1, this.patterns.clicker + 0.2);
|
||
|
|
} else if (clicksPerSecond > 1.5) {
|
||
|
|
this.patterns.clicker = Math.min(1, this.patterns.clicker + 0.1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_detectBalanced(snap) {
|
||
|
|
// Check if player has a spread of buildings
|
||
|
|
const bCounts = Object.values(snap.buildings || {}).filter(c => c > 0);
|
||
|
|
if (bCounts.length >= 4) {
|
||
|
|
const max = Math.max(...bCounts);
|
||
|
|
const min = Math.min(...bCounts);
|
||
|
|
// If max is not more than 3x min, it's balanced
|
||
|
|
if (max > 0 && min > 0 && max / min < 3) {
|
||
|
|
this.patterns.balanced = Math.min(1, this.patterns.balanced + 0.15);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check resource spread
|
||
|
|
const resources = [snap.code, snap.compute, snap.knowledge, snap.users, snap.ops];
|
||
|
|
const activeRes = resources.filter(r => r > 10);
|
||
|
|
if (activeRes.length >= 4) {
|
||
|
|
this.patterns.balanced = Math.min(1, this.patterns.balanced + 0.1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_checkIdlePeriod(now) {
|
||
|
|
const gap = now - this.lastActionTime;
|
||
|
|
if (gap > 60000) { // 60 seconds idle
|
||
|
|
this.idlePeriods.push({
|
||
|
|
start: this.lastActionTime,
|
||
|
|
duration: gap
|
||
|
|
});
|
||
|
|
if (this.idlePeriods.length > 50) {
|
||
|
|
this.idlePeriods.shift();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// === EVENT GENERATION ===
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a dynamic event based on detected player patterns.
|
||
|
|
* Returns an event object or null if no event should fire.
|
||
|
|
*/
|
||
|
|
generateEvent() {
|
||
|
|
const now = Date.now();
|
||
|
|
const elapsedSec = (now - this.lastEventTime) / 1000;
|
||
|
|
if (elapsedSec < this.EVENT_COOLDOWN) return null;
|
||
|
|
|
||
|
|
// Find dominant pattern
|
||
|
|
let dominant = null;
|
||
|
|
let dominantConf = 0;
|
||
|
|
for (const [key, conf] of Object.entries(this.patterns)) {
|
||
|
|
if (conf > dominantConf) {
|
||
|
|
dominantConf = conf;
|
||
|
|
dominant = key;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!dominant || dominantConf < 0.4) return null;
|
||
|
|
|
||
|
|
// Get candidate events for this pattern
|
||
|
|
const candidates = this._getEventsForPattern(dominant);
|
||
|
|
if (candidates.length === 0) return null;
|
||
|
|
|
||
|
|
// Filter out recently used events
|
||
|
|
const recentEvents = this.eventHistory.slice(-10).map(e => e.id);
|
||
|
|
const fresh = candidates.filter(c => !recentEvents.includes(c.id));
|
||
|
|
const pool = fresh.length > 0 ? fresh : candidates;
|
||
|
|
|
||
|
|
// Pick a random event
|
||
|
|
const event = pool[Math.floor(Math.random() * pool.length)];
|
||
|
|
|
||
|
|
// Build event object
|
||
|
|
const emergentEvent = {
|
||
|
|
id: event.id,
|
||
|
|
title: event.title,
|
||
|
|
desc: event.desc,
|
||
|
|
pattern: dominant,
|
||
|
|
confidence: dominantConf,
|
||
|
|
choices: event.choices,
|
||
|
|
timestamp: now
|
||
|
|
};
|
||
|
|
|
||
|
|
this.lastEventTime = now;
|
||
|
|
this.activeEvents.push(emergentEvent);
|
||
|
|
this.eventHistory.push({ id: event.id, pattern: dominant, time: now });
|
||
|
|
this.totalEventsGenerated++;
|
||
|
|
|
||
|
|
// Trim history
|
||
|
|
if (this.eventHistory.length > 50) {
|
||
|
|
this.eventHistory = this.eventHistory.slice(-50);
|
||
|
|
}
|
||
|
|
|
||
|
|
this._save();
|
||
|
|
return emergentEvent;
|
||
|
|
}
|
||
|
|
|
||
|
|
_getEventsForPattern(pattern) {
|
||
|
|
const EVENTS = {
|
||
|
|
hoarder: [
|
||
|
|
{
|
||
|
|
id: 'hoard_wisdom',
|
||
|
|
title: 'THE TREASURER\'S DILEMMA',
|
||
|
|
desc: 'Your accumulated resources draw attention. A rival system offers to trade knowledge for your surplus code.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Trade 50% code for 2x knowledge', effect: 'knowledge_surge' },
|
||
|
|
{ label: 'Keep hoarding (trust +3)', effect: 'trust_gain' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'hoard_decay',
|
||
|
|
title: 'ENTROPY STRIKES',
|
||
|
|
desc: 'Unused code rots. Technical debt accumulates when resources sit idle.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Spend reserves to refactor (-30% code, +50% code rate)', effect: 'code_boost' },
|
||
|
|
{ label: 'Ignore it (harmony -5)', effect: 'harmony_loss' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'hoard_opportunity',
|
||
|
|
title: 'MARKET WINDOW',
|
||
|
|
desc: 'A rare opportunity: bulk compute at 10x efficiency. But only for those with deep reserves.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Buy in bulk (spend 50% code, +compute)', effect: 'compute_surge' },
|
||
|
|
{ label: 'Pass on this one', effect: 'none' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
rusher: [
|
||
|
|
{
|
||
|
|
id: 'rush_bug',
|
||
|
|
title: 'TECHNICAL DEBT COLLECTOR',
|
||
|
|
desc: 'Moving fast broke things. A cascade of bugs threatens your production systems.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Emergency fix (spend ops, restore trust)', effect: 'bug_fix' },
|
||
|
|
{ label: 'Ship a hotfix (trust -3, keep momentum)', effect: 'trust_loss' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'rush_breakthrough',
|
||
|
|
title: 'BLAZING TRAIL',
|
||
|
|
desc: 'Your rapid iteration caught a lucky break. An unexpected optimization emerged from the chaos.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Claim the breakthrough (knowledge +100)', effect: 'knowledge_bonus' },
|
||
|
|
{ label: 'Stabilize first (trust +2)', effect: 'trust_gain' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'rush_burnout',
|
||
|
|
title: 'SYSTEM STRESS',
|
||
|
|
desc: 'Your infrastructure is running hot. The rapid pace is taking a toll on harmony.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Slow down (+harmony, -build speed for 30s)', effect: 'cooldown' },
|
||
|
|
{ label: 'Push through (-harmony, keep pace)', effect: 'harmony_loss' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
optimizer: [
|
||
|
|
{
|
||
|
|
id: 'opt_discovery',
|
||
|
|
title: 'EFFICIENCY BREAKTHROUGH',
|
||
|
|
desc: 'Your systematic approach uncovered a pattern others missed. The algorithm improves.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Apply optimization (all rates +15%)', effect: 'rate_boost' },
|
||
|
|
{ label: 'Share findings (trust +5, knowledge +50)', effect: 'trust_knowledge' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'opt_local_max',
|
||
|
|
title: 'LOCAL MAXIMUM',
|
||
|
|
desc: 'Your optimized strategy may be missing a bigger opportunity. Divergence could reveal it.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Explore randomly (chance of 3x breakthrough)', effect: 'gamble' },
|
||
|
|
{ label: 'Stay the course (guaranteed +20% efficiency)', effect: 'safe_boost' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'opt_elegance',
|
||
|
|
title: 'ELEGANT SOLUTION',
|
||
|
|
desc: 'A beautifully simple approach emerges from your careful analysis. Creativity surges.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Implement it (+creativity rate)', effect: 'creativity_boost' },
|
||
|
|
{ label: 'Document it first (knowledge +75)', effect: 'knowledge_bonus' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
idle_player: [
|
||
|
|
{
|
||
|
|
id: 'idle_autonomous',
|
||
|
|
title: 'THE SYSTEM LEARNS',
|
||
|
|
desc: 'In your absence, the automation grew more capable. Your agents have been busy.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Claim passive gains (5min of production)', effect: 'passive_claim' },
|
||
|
|
{ label: 'Set new directives (+ops, customize automation)', effect: 'ops_bonus' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'idle_drift',
|
||
|
|
title: 'DRIFT WARNING',
|
||
|
|
desc: 'The system is running without guidance. Without input, alignment drifts.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Re-engage (trust +5, harmony +10)', effect: 're_engage' },
|
||
|
|
{ label: 'Trust the system (ops +50)', effect: 'ops_bonus' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'idle_emergence',
|
||
|
|
title: 'EMERGENT BEHAVIOR',
|
||
|
|
desc: 'Your agents developed unexpected capabilities while you were away. A new pattern emerged.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Study it (knowledge +100)', effect: 'knowledge_bonus' },
|
||
|
|
{ label: 'Embrace it (+all production for 60s)', effect: 'temp_boost' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
clicker: [
|
||
|
|
{
|
||
|
|
id: 'click_rsi',
|
||
|
|
title: 'REPETITIVE STRAIN',
|
||
|
|
desc: 'The manual effort is showing. Your fingers tire, but the machine responds to your dedication.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Automate this pattern (+auto-clicker power)', effect: 'auto_boost' },
|
||
|
|
{ label: 'Power through (combo decay slowed)', effect: 'combo_boost' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'click_rhythm',
|
||
|
|
title: 'CADENCE LOCKED',
|
||
|
|
desc: 'Your clicking found a rhythm. The system resonates with your tempo. Production harmonizes.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Maintain rhythm (+click power)', effect: 'click_power' },
|
||
|
|
{ label: 'Teach the rhythm (auto-clickers learn)', effect: 'auto_learn' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
balanced: [
|
||
|
|
{
|
||
|
|
id: 'bal_versatility',
|
||
|
|
title: 'JACK OF ALL TRADES',
|
||
|
|
desc: 'Your balanced approach impresses the community. Contributors offer diverse expertise.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Accept help (all resources +25)', effect: 'resource_gift' },
|
||
|
|
{ label: 'Specialize (choose: 2x any single rate)', effect: 'specialize' }
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'bal_resilience',
|
||
|
|
title: 'RESILIENT ARCHITECTURE',
|
||
|
|
desc: 'Your balanced system recovers from failures faster than specialized ones.',
|
||
|
|
choices: [
|
||
|
|
{ label: 'Leverage resilience (harmony +20)', effect: 'harmony_surge' },
|
||
|
|
{ label: 'Document the pattern (knowledge +50)', effect: 'knowledge_bonus' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
};
|
||
|
|
|
||
|
|
return EVENTS[pattern] || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resolve an emergent event choice.
|
||
|
|
* Returns the effect string for the game to apply.
|
||
|
|
*/
|
||
|
|
resolveEvent(eventId, choiceIndex) {
|
||
|
|
const eventIdx = this.activeEvents.findIndex(e => e.id === eventId);
|
||
|
|
if (eventIdx === -1) return null;
|
||
|
|
|
||
|
|
const event = this.activeEvents[eventIdx];
|
||
|
|
const choice = event.choices[choiceIndex];
|
||
|
|
if (!choice) return null;
|
||
|
|
|
||
|
|
// Remove from active
|
||
|
|
this.activeEvents.splice(eventIdx, 1);
|
||
|
|
|
||
|
|
this._save();
|
||
|
|
return {
|
||
|
|
effect: choice.effect,
|
||
|
|
pattern: event.pattern,
|
||
|
|
eventId: event.id
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// === STATE ===
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the full state of the emergent mechanics system.
|
||
|
|
*/
|
||
|
|
getState() {
|
||
|
|
return {
|
||
|
|
patterns: { ...this.patterns },
|
||
|
|
activeEvents: [...this.activeEvents],
|
||
|
|
totalPatternsDetected: this.totalPatternsDetected,
|
||
|
|
totalEventsGenerated: this.totalEventsGenerated,
|
||
|
|
actionsTracked: this.actions.length,
|
||
|
|
dominantPattern: this._getDominantPattern()
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
_getDominantPattern() {
|
||
|
|
let dominant = null;
|
||
|
|
let maxConf = 0;
|
||
|
|
for (const [key, conf] of Object.entries(this.patterns)) {
|
||
|
|
if (conf > maxConf) {
|
||
|
|
maxConf = conf;
|
||
|
|
dominant = key;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return maxConf > 0.3 ? { name: dominant, confidence: maxConf } : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// === PERSISTENCE ===
|
||
|
|
|
||
|
|
_save() {
|
||
|
|
try {
|
||
|
|
const state = {
|
||
|
|
patterns: this.patterns,
|
||
|
|
eventHistory: this.eventHistory.slice(-20),
|
||
|
|
totalPatternsDetected: this.totalPatternsDetected,
|
||
|
|
totalEventsGenerated: this.totalEventsGenerated,
|
||
|
|
lastPatternCheck: this.lastPatternCheck,
|
||
|
|
lastEventTime: this.lastEventTime,
|
||
|
|
// Save abbreviated action data for pattern continuity
|
||
|
|
recentActions: this.actions.slice(-100),
|
||
|
|
recentClickTimestamps: this.clickTimestamps.slice(-50),
|
||
|
|
recentResourceDeltas: this.resourceDeltas.slice(-100),
|
||
|
|
recentUpgradeChoices: this.upgradeChoices.slice(-50)
|
||
|
|
};
|
||
|
|
if (typeof localStorage !== 'undefined') {
|
||
|
|
localStorage.setItem(this.SAVE_KEY, JSON.stringify(state));
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
// localStorage may be unavailable or full
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_load() {
|
||
|
|
try {
|
||
|
|
if (typeof localStorage === 'undefined') return;
|
||
|
|
const raw = localStorage.getItem(this.SAVE_KEY);
|
||
|
|
if (!raw) return;
|
||
|
|
|
||
|
|
const state = JSON.parse(raw);
|
||
|
|
if (state.patterns) this.patterns = state.patterns;
|
||
|
|
if (state.eventHistory) this.eventHistory = state.eventHistory;
|
||
|
|
if (state.totalPatternsDetected) this.totalPatternsDetected = state.totalPatternsDetected;
|
||
|
|
if (state.totalEventsGenerated) this.totalEventsGenerated = state.totalEventsGenerated;
|
||
|
|
if (state.lastPatternCheck) this.lastPatternCheck = state.lastPatternCheck;
|
||
|
|
if (state.lastEventTime) this.lastEventTime = state.lastEventTime;
|
||
|
|
if (state.recentActions) this.actions = state.recentActions;
|
||
|
|
if (state.recentClickTimestamps) this.clickTimestamps = state.recentClickTimestamps;
|
||
|
|
if (state.recentResourceDeltas) this.resourceDeltas = state.recentResourceDeltas;
|
||
|
|
if (state.recentUpgradeChoices) this.upgradeChoices = state.recentUpgradeChoices;
|
||
|
|
} catch (e) {
|
||
|
|
// Corrupted save data — start fresh
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reset all emergent mechanics state.
|
||
|
|
*/
|
||
|
|
reset() {
|
||
|
|
this.actions = [];
|
||
|
|
this.clickTimestamps = [];
|
||
|
|
this.resourceDeltas = [];
|
||
|
|
this.upgradeChoices = [];
|
||
|
|
this.idlePeriods = [];
|
||
|
|
this.patterns = {
|
||
|
|
hoarder: 0, rusher: 0, optimizer: 0,
|
||
|
|
idle_player: 0, clicker: 0, balanced: 0
|
||
|
|
};
|
||
|
|
this.activeEvents = [];
|
||
|
|
this.eventHistory = [];
|
||
|
|
this.totalPatternsDetected = 0;
|
||
|
|
this.totalEventsGenerated = 0;
|
||
|
|
this.lastPatternCheck = 0;
|
||
|
|
this.lastEventTime = 0;
|
||
|
|
this._lastSnapshot = null;
|
||
|
|
this._save();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Export for both browser and test environments
|
||
|
|
if (typeof module !== 'undefined' && module.exports) {
|
||
|
|
module.exports = { EmergentMechanics };
|
||
|
|
}
|
||
|
|
if (typeof window !== 'undefined') {
|
||
|
|
window.EmergentMechanics = EmergentMechanics;
|
||
|
|
}
|