THE MOTEL - Personalization System Technical Specification
Executive Summary
This document provides a complete technical specification for THE MOTEL’s cookie-based personalization and progression system. The system enables 40 unique doors (existential threat experiences) to be dynamically assigned, tracked, and evolved based on each visitor’s journey, while ensuring friends have distinct experiences.
1. COOKIE/LOCAL STORAGE ARCHITECTURE
1.1 Data Structure Overview
// Primary Visitor State Object
const MOTEL_STATE_SCHEMA = {
// Core Identity
visitorId: "uuid-v4-string", // Anonymous unique identifier
visitorHash: "sha256-derived", // Seed for deterministic generation
firstVisit: "ISO-8601-timestamp",
lastVisit: "ISO-8601-timestamp",
visitCount: 0,
// Progress Tracking
totalProgress: 0.0, // 0.0 to 1.0 (percentage)
doorsUnlocked: [], // Array of door IDs
doorsCompleted: [], // Array of completed door IDs
doorsAbandoned: [], // Started but not completed
// Door Configuration (Personalized)
doorMapping: { // Maps slot positions to actual doors
"slot_01": "door_07",
"slot_02": "door_23",
// ... 40 slots mapped to 40 doors
},
mysteryDoorSlot: "slot_17", // Which slot holds the mystery door
// Per-Door State
doorStates: {
"door_07": {
discovered: true,
visited: true,
completed: true,
completionTime: 245000, // milliseconds
choicesMade: ["choice_a", "choice_c"],
attempts: 1,
lastVisited: "ISO-8601-timestamp"
}
},
// Session State
currentPosition: "hallway", // hallway, door_XX, ending_XX
lastDoorVisited: null,
sessionStartTime: "ISO-8601-timestamp",
totalTimeInMotel: 0, // cumulative milliseconds
// Milestones & Unlocks
milestonesReached: ["first_door", "halfway"],
easterEggsFound: ["secret_message_03"],
newGamePlusUnlocked: false,
// Preferences
settings: {
audioEnabled: true,
reducedMotion: false,
highContrast: false
},
// Versioning
schemaVersion: "1.0.0",
lastMigrated: "ISO-8601-timestamp"
};1.2 Cookie Naming Conventions
const COOKIE_NAMES = {
// Primary State (compressed, base64)
PRIMARY_STATE: 'motel_visitor_state',
// Session-only cookies (deleted on browser close)
SESSION_ID: 'motel_session_id',
CURRENT_POSITION: 'motel_current_pos',
// Lightweight flags (for quick checks)
RETURNING_VISITOR: 'motel_returning',
PROGRESS_PERCENT: 'motel_progress',
// Friend differentiation
FRIEND_CODE: 'motel_friend_code', // Optional: shared friend code
// Analytics (anonymous)
ANALYTICS_CONSENT: 'motel_analytics_ok'
};1.3 Storage Strategy & Limits
const STORAGE_CONFIG = {
// Cookie storage (primary, server-readable)
cookies: {
maxSize: 4000, // bytes per cookie (browser safe)
strategy: 'compressed_primary', // LZ-string compression
expiration: 365, // days
secure: true,
sameSite: 'Lax'
},
// localStorage (backup & extended data)
localStorage: {
key: 'motel_extended_state',
maxSize: 5000000, // 5MB typical limit
useFor: ['doorStates', 'easterEggsFound', 'detailedAnalytics']
},
// sessionStorage (temporary session data)
sessionStorage: {
key: 'motel_session_cache',
useFor: ['currentAnimationState', 'scrollPosition', 'temporaryChoices']
},
// IndexedDB (future-proofing for rich media)
indexedDB: {
dbName: 'MotelExperienceDB',
version: 1,
stores: ['mediaCache', 'offlineProgress']
}
};1.4 Compression Strategy
// LZ-String compression for cookie storage
class StateCompressor {
static compress(state) {
const json = JSON.stringify(state);
return LZString.compressToBase64(json);
}
static decompress(compressed) {
const json = LZString.decompressFromBase64(compressed);
return JSON.parse(json);
}
// Estimated sizes
static estimateSize(state) {
const compressed = this.compress(state);
return {
raw: JSON.stringify(state).length,
compressed: compressed.length,
ratio: compressed.length / JSON.stringify(state).length
};
}
}
// Typical compression: 60-70% reduction
// Full state (~3KB raw) → ~1KB compressed1.5 Privacy & GDPR Compliance
const PRIVACY_CONFIG = {
// Data collected (all anonymous)
collectedData: {
visitorId: 'anonymous_uuid', // No PII linkage
progress: 'game_state_only', // No personal info
choices: 'in_game_decisions', // No external correlation
timing: 'session_duration' // No fingerprinting
},
// GDPR considerations
gdpr: {
// No personal data stored
noPii: true,
// Visitor can request data deletion
rightToErasure: true,
// Data is functional, not tracking
legitimateInterest: true,
// Clear cookie banner required
consentRequired: true,
// Data retention
retentionDays: 365,
// Automatic cleanup
autoDeleteInactive: 730 // 2 years
},
// Cookie banner text
consentText: {
title: "THE MOTEL remembers its guests",
description: "We use cookies to remember your progress through the hallway. No personal data is stored.",
accept: "Enter the Motel",
decline: "Remain Anonymous (progress won't be saved)"
}
};1.6 Data Expiration & Reset Mechanics
class DataLifecycleManager {
// Check if state needs migration
static checkVersion(state) {
const CURRENT_VERSION = '1.0.0';
if (!state.schemaVersion || state.schemaVersion !== CURRENT_VERSION) {
return this.migrateState(state, CURRENT_VERSION);
}
return state;
}
// Migrate old state to new schema
static migrateState(oldState, targetVersion) {
// Migration logic for future updates
const migrations = {
'0.9.0_to_1.0.0': (state) => {
// Add new fields, transform old ones
state.schemaVersion = '1.0.0';
state.mysteryDoorSlot = state.mysteryDoorSlot || null;
return state;
}
};
return migrations[`${oldState.schemaVersion}_to_${targetVersion}`]?.(oldState) || oldState;
}
// Reset options
static resetOptions = {
SOFT_RESET: 'soft', // Keep visitor ID, reset progress
HARD_RESET: 'hard', // New visitor entirely
NEW_GAME_PLUS: 'ngp', // Keep unlocks, reshuffle doors
DOOR_ONLY: 'door' // Reset specific door
};
static reset(state, option, targetDoor = null) {
switch(option) {
case 'soft':
return {
...state,
totalProgress: 0,
doorsUnlocked: [],
doorsCompleted: [],
doorsAbandoned: [],
doorStates: {},
milestonesReached: [],
easterEggsFound: []
};
case 'hard':
return null; // Delete all cookies
case 'ngp':
return {
...state,
doorMapping: this.generateNewMapping(state.visitorHash + '_ngp'),
totalProgress: 0,
doorsCompleted: [],
newGamePlusUnlocked: false
};
case 'door':
if (targetDoor) {
const newDoorStates = { ...state.doorStates };
delete newDoorStates[targetDoor];
return {
...state,
doorStates: newDoorStates,
doorsCompleted: state.doorsCompleted.filter(d => d !== targetDoor)
};
}
return state;
}
}
// Auto-cleanup inactive visitors
static shouldCleanup(state) {
const INACTIVE_THRESHOLD = 730 * 24 * 60 * 60 * 1000; // 2 years
const lastVisit = new Date(state.lastVisit).getTime();
return Date.now() - lastVisit > INACTIVE_THRESHOLD;
}
}2. PROGRESSION SYSTEM
2.1 Door Unlock Mechanics
const PROGRESSION_RULES = {
// Initial state
startingDoors: {
unlocked: ['slot_01'], // First door always unlocked
visible: ['slot_01', 'slot_02'] // Next door teased
},
// Unlock conditions
unlockRules: [
{
condition: 'first_completion',
trigger: (state) => state.doorsCompleted.length === 1,
unlockSlots: ['slot_02', 'slot_03'],
message: "The hallway extends..."
},
{
condition: 'milestone_5',
trigger: (state) => state.doorsCompleted.length === 5,
unlockSlots: ['slot_04', 'slot_05', 'slot_06'],
revealMystery: false,
message: "You sense something watching..."
},
{
condition: 'milestone_10',
trigger: (state) => state.doorsCompleted.length === 10,
unlockSlots: ['slot_07', 'slot_08', 'slot_09', 'slot_10'],
revealMystery: true,
message: "A door you've never noticed before..."
},
{
condition: 'milestone_20',
trigger: (state) => state.doorsCompleted.length === 20,
unlockSlots: ['slot_11', 'slot_12', 'slot_13', 'slot_14', 'slot_15'],
message: "Halfway. The motel knows you now."
},
{
condition: 'milestone_30',
trigger: (state) => state.doorsCompleted.length === 30,
unlockSlots: ['slot_16', 'slot_17', 'slot_18', 'slot_19', 'slot_20'],
message: "The end approaches. But does it?"
},
{
condition: 'completion',
trigger: (state) => state.doorsCompleted.length === 40,
unlockSlots: [],
unlockEnding: true,
message: "Check-out time."
}
],
// Progressive reveal
revealStrategy: {
type: 'staggered', // Not all doors visible at once
initialVisible: 3,
incrementOnUnlock: 2,
maxVisibleBeforeCompletion: 25 // Mystery maintained
}
};2.2 Completion Tracking
class ProgressTracker {
static calculateProgress(state) {
const totalDoors = 40;
const completed = state.doorsCompleted.length;
// Weight factors
const weights = {
completion: 0.7, // 70% for completing doors
discovery: 0.2, // 20% for discovering doors
exploration: 0.1 // 10% for time/engagement
};
const completionScore = (completed / totalDoors) * weights.completion;
const discoveryScore = (state.doorsUnlocked.length / totalDoors) * weights.discovery;
const explorationScore = Math.min(
state.totalTimeInMotel / (60 * 60 * 1000), // Cap at 1 hour
1
) * weights.exploration;
return Math.min(completionScore + discoveryScore + explorationScore, 1.0);
}
static getProgressTier(progress) {
if (progress === 0) return 'newcomer';
if (progress < 0.25) return 'wanderer';
if (progress < 0.5) return 'explorer';
if (progress < 0.75) return 'regular';
if (progress < 1.0) return 'resident';
return 'permanent_guest';
}
static getTierBenefits(tier) {
const benefits = {
newcomer: ['basic_access'],
wanderer: ['basic_access', 'hint_system'],
explorer: ['basic_access', 'hint_system', 'skip_option'],
regular: ['basic_access', 'hint_system', 'skip_option', 'behind_scenes'],
resident: ['basic_access', 'hint_system', 'skip_option', 'behind_scenes', 'director_commentary'],
permanent_guest: ['all_access', 'new_game_plus', 'door_editor']
};
return benefits[tier] || benefits.newcomer;
}
}2.3 Milestone System
const MILESTONES = {
// Discovery milestones
'first_door': {
condition: (s) => s.doorsCompleted.length >= 1,
reward: 'unlocked_hint_system',
message: "You've taken your first step."
},
'fifth_door': {
condition: (s) => s.doorsCompleted.length >= 5,
reward: 'unlocked_skip_single_door',
message: "The motel recognizes persistence."
},
'tenth_door': {
condition: (s) => s.doorsCompleted.length >= 10,
reward: 'revealed_mystery_door',
message: "Not all doors are where they appear."
},
// Progress milestones
'halfway': {
condition: (s) => s.doorsCompleted.length >= 20,
reward: 'unlocked_behind_scenes',
message: "Twenty doors. The motel knows your fears."
},
'three_quarters': {
condition: (s) => s.doorsCompleted.length >= 30,
reward: 'unlocked_director_commentary',
message: "Almost there. Or are you?"
},
// Completion milestones
'completion': {
condition: (s) => s.doorsCompleted.length >= 40,
reward: 'unlocked_new_game_plus',
message: "You've seen all the motel has to offer. For now."
},
// Special milestones
'speed_run': {
condition: (s) => s.doorsCompleted.length >= 5 && s.totalTimeInMotel < 30 * 60 * 1000,
reward: 'speed_runner_badge',
message: "Moving quickly through the darkness."
},
'completionist': {
condition: (s) => Object.keys(s.doorStates).every(d => s.doorStates[d].completed),
reward: 'perfectionist_badge',
message: "Every door holds a story. You've heard them all."
},
'return_visitor': {
condition: (s) => s.visitCount >= 5,
reward: 'welcome_home_message',
message: "The motel remembers those who return."
}
};2.4 New Game Plus
class NewGamePlusManager {
static isUnlocked(state) {
return state.doorsCompleted.length === 40;
}
static startNewGamePlus(state) {
// Generate new door arrangement with same visitor
const newSeed = state.visitorHash + '_ngp_' + Date.now();
return {
...state,
visitorHash: newSeed,
doorMapping: this.generateMapping(newSeed),
totalProgress: 0,
doorsUnlocked: [],
doorsCompleted: [],
doorsAbandoned: [],
doorStates: {},
currentPosition: 'hallway',
newGamePlusUnlocked: true,
ngpCount: (state.ngpCount || 0) + 1,
// Carry over
easterEggsFound: state.easterEggsFound, // Permanent unlocks
milestonesReached: state.milestonesReached,
totalTimeInMotel: state.totalTimeInMotel
};
}
static getNGPModifiers(ngpCount) {
return {
// Each NG+ makes doors... different
doorIntensity: Math.min(1 + (ngpCount * 0.1), 2.0), // Max 2x intensity
hiddenDoors: Math.min(ngpCount * 2, 10), // Extra hidden doors
mysteryDoorChanges: ngpCount >= 2, // Mystery moves
alternateEndings: ngpCount >= 3 // New endings
};
}
}3. PERSONALIZATION ALGORITHMS
3.1 Visitor Hash Generation
class VisitorIdentity {
static generateVisitorId() {
// UUID v4
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
static generateVisitorHash(visitorId, fingerprint = '') {
// Deterministic hash from visitor ID + optional fingerprint
// Fingerprint could include: user agent (hashed), screen size, timezone
const combined = visitorId + fingerprint;
return this.simpleHash(combined);
}
static simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
// Seeded random number generator (deterministic)
static seededRandom(seed) {
let s = parseInt(seed, 16) || 12345;
return () => {
s = Math.sin(s) * 10000;
return s - Math.floor(s);
};
}
}3.2 Door Mapping Generation
class DoorMappingGenerator {
// 40 doors, each visitor gets a unique arrangement
static generateMapping(visitorHash) {
const rng = VisitorIdentity.seededRandom(visitorHash);
// All 40 door IDs
const allDoors = Array.from({length: 40}, (_, i) => `door_${String(i + 1).padStart(2, '0')}`);
// Fisher-Yates shuffle with seeded RNG
const shuffled = [...allDoors];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(rng() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
// Create slot mapping
const mapping = {};
shuffled.forEach((door, index) => {
mapping[`slot_${String(index + 1).padStart(2, '0')}`] = door;
});
return mapping;
}
// Get which door is at a specific slot
static getDoorAtSlot(mapping, slotNumber) {
const slotKey = `slot_${String(slotNumber).padStart(2, '0')}`;
return mapping[slotKey];
}
// Get slot number for a specific door
static getSlotForDoor(mapping, doorId) {
return Object.entries(mapping).find(([slot, door]) => door === doorId)?.[0];
}
}3.3 Mystery Door Assignment
class MysteryDoorManager {
static assignMysteryDoor(visitorHash, mapping) {
const rng = VisitorIdentity.seededRandom(visitorHash + '_mystery');
// Mystery door appears at slot 15-35 (not too early, not at end)
const minSlot = 15;
const maxSlot = 35;
const mysterySlotNumber = Math.floor(rng() * (maxSlot - minSlot + 1)) + minSlot;
return {
slot: `slot_${String(mysterySlotNumber).padStart(2, '0')}`,
doorId: mapping[`slot_${String(mysterySlotNumber).padStart(2, '0')}`],
revealed: false,
accessible: false
};
}
// Mystery door reveals itself under certain conditions
static checkRevealConditions(state, mysteryDoor) {
const completed = state.doorsCompleted.length;
// Reveal after 10 doors completed
if (completed >= 10 && !mysteryDoor.revealed) {
return { revealed: true, accessible: false };
}
// Become accessible after 20 doors
if (completed >= 20 && mysteryDoor.revealed && !mysteryDoor.accessible) {
return { revealed: true, accessible: true };
}
return mysteryDoor;
}
// What the mystery door contains
static getMysteryContent(visitorHash) {
const rng = VisitorIdentity.seededRandom(visitorHash + '_mystery_content');
const mysteryTypes = [
'reflection', // Mirror of visitor's choices
'revelation', // Hidden truth about the motel
'choice', // A door that asks a question
'memory', // Recalls previous doors
'meta', // Breaks fourth wall
'gift' // Unlocks something special
];
return mysteryTypes[Math.floor(rng() * mysteryTypes.length)];
}
}3.4 Visible/Hidden Door Logic
class DoorVisibilityManager {
static getVisibleDoors(state, includeTeasers = true) {
const visible = [];
const unlockedCount = state.doorsUnlocked.length;
// Always show unlocked doors
state.doorsUnlocked.forEach(slot => {
visible.push({
slot,
doorId: state.doorMapping[slot],
status: 'unlocked',
accessible: true
});
});
// Show teaser for next door(s)
if (includeTeasers) {
const nextSlotNumber = unlockedCount + 1;
const nextSlot = `slot_${String(nextSlotNumber).padStart(2, '0')}`;
if (state.doorMapping[nextSlot] && !state.doorsUnlocked.includes(nextSlot)) {
visible.push({
slot: nextSlot,
doorId: null, // Hidden until unlocked
status: 'teaser',
accessible: false
});
}
}
// Mystery door (if revealed)
if (state.mysteryDoorSlot) {
const mysteryState = state.doorStates[state.doorMapping[state.mysteryDoorSlot]];
if (mysteryState?.discovered) {
visible.push({
slot: state.mysteryDoorSlot,
doorId: state.doorMapping[state.mysteryDoorSlot],
status: 'mystery',
accessible: mysteryState?.accessible || false
});
}
}
return visible;
}
// Get door appearance based on state
static getDoorAppearance(doorId, doorState) {
const appearances = {
locked: { style: 'dark', glow: 'none', label: '???' },
unlocked: { style: 'wood', glow: 'subtle', label: doorId },
visited: { style: 'wood', glow: 'warm', label: doorId, mark: 'visited' },
completed: { style: 'aged', glow: 'golden', label: doorId, mark: 'completed' },
mystery: { style: 'shifting', glow: 'pulsing', label: '?' },
abandoned: { style: 'cracked', glow: 'flickering', label: doorId }
};
if (!doorState) return appearances.locked;
if (doorState.completed) return appearances.completed;
if (doorState.visited) return appearances.visited;
return appearances.unlocked;
}
}3.5 Recommended Next Door Algorithm
class RecommendationEngine {
static getRecommendedDoor(state) {
const unlockedDoors = state.doorsUnlocked
.map(slot => ({
slot,
doorId: state.doorMapping[slot],
state: state.doorStates[state.doorMapping[slot]]
}))
.filter(d => !d.state?.completed); // Only incomplete doors
if (unlockedDoors.length === 0) {
return null; // All unlocked doors completed
}
// Score each door
const scoredDoors = unlockedDoors.map(door => ({
...door,
score: this.calculateDoorScore(door, state)
}));
// Sort by score (highest first)
scoredDoors.sort((a, b) => b.score - a.score);
return scoredDoors[0];
}
static calculateDoorScore(door, state) {
let score = 0;
const doorState = door.state || {};
// Never visited = high priority
if (!doorState.visited) {
score += 100;
}
// Started but abandoned = medium priority
if (doorState.visited && !doorState.completed) {
score += 50;
}
// Time since last visit (prefer doors not visited recently)
if (doorState.lastVisited) {
const hoursSince = (Date.now() - new Date(doorState.lastVisited).getTime()) / (1000 * 60 * 60);
score += Math.min(hoursSince / 24, 30); // Max 30 points for time
}
// Door number preference (slight preference for earlier doors)
const doorNum = parseInt(door.doorId.split('_')[1]);
score += (40 - doorNum) * 0.5;
// Mystery door bonus (if accessible)
if (door.slot === state.mysteryDoorSlot && doorState.accessible) {
score += 200;
}
return score;
}
// Get recommendation message
static getRecommendationMessage(recommendedDoor, state) {
if (!recommendedDoor) {
return "All doors await your return.";
}
const doorNum = parseInt(recommendedDoor.doorId.split('_')[1]);
const messages = {
neverVisited: [
"A door you haven't opened calls to you.",
"Something waits behind Door {n}.",
"Door {n} remains unexplored."
],
abandoned: [
"You left something behind at Door {n}.",
"Door {n} remembers your hesitation.",
"Perhaps you're ready for Door {n} now."
],
mystery: [
"The mystery door beckons.",
"Some doors appear only to those who've seen enough."
]
};
const doorState = recommendedDoor.state || {};
let category = 'neverVisited';
if (recommendedDoor.slot === state.mysteryDoorSlot) {
category = 'mystery';
} else if (doorState.visited && !doorState.completed) {
category = 'abandoned';
}
const msgs = messages[category];
const msg = msgs[Math.floor(Math.random() * msgs.length)];
return msg.replace('{n}', doorNum);
}
}4. FRIEND DIFFERENTIATION
4.1 Friend Code System
class FriendSystem {
// Generate a shareable friend code
static generateFriendCode(visitorId) {
// Create a short, shareable code
const hash = VisitorIdentity.simpleHash(visitorId);
return hash.substring(0, 8).toUpperCase();
}
// Parse friend code to get comparison data
static parseFriendCode(code) {
// In production, this would lookup the friend's state
// For now, we'll use the code as a seed
return {
friendHash: code,
canCompare: true
};
}
// Compare two visitors' progress
static compareProgress(myState, friendState) {
const myCompleted = new Set(myState.doorsCompleted);
const friendCompleted = new Set(friendState.doorsCompleted);
// Doors I've done that friend hasn't
const onlyMine = [...myCompleted].filter(d => !friendCompleted.has(d));
// Doors friend has done that I haven't
const onlyFriends = [...friendCompleted].filter(d => !myCompleted.has(d));
// Doors we've both done
const both = [...myCompleted].filter(d => friendCompleted.has(d));
return {
myProgress: myState.totalProgress,
friendProgress: friendState.totalProgress,
onlyMine,
onlyFriends,
both,
aheadBy: myCompleted.size - friendCompleted.size
};
}
}4.2 Different Door Assignments
class DifferentiationEngine {
// Ensure friends see different door arrangements
static generateDifferentiatedMapping(visitorHash, friendHashes = []) {
// Start with base mapping
let mapping = DoorMappingGenerator.generateMapping(visitorHash);
// If we have friend hashes, ensure differentiation
if (friendHashes.length > 0) {
// Get friend mappings
const friendMappings = friendHashes.map(h => DoorMappingGenerator.generateMapping(h));
// Check for similarity
let attempts = 0;
const maxAttempts = 100;
while (attempts < maxAttempts) {
const similarity = this.calculateSimilarity(mapping, friendMappings);
if (similarity < 0.3) { // Less than 30% similar
break; // Good enough
}
// Regenerate with modified seed
const newSeed = visitorHash + '_diff_' + attempts;
mapping = DoorMappingGenerator.generateMapping(newSeed);
attempts++;
}
}
return mapping;
}
static calculateSimilarity(mapping, friendMappings) {
let totalSimilarity = 0;
friendMappings.forEach(friendMap => {
let same = 0;
let total = 0;
Object.entries(mapping).forEach(([slot, door]) => {
if (friendMap[slot] === door) {
same++;
}
total++;
});
totalSimilarity += same / total;
});
return totalSimilarity / friendMappings.length;
}
}4.3 Shared vs Private Experiences
const EXPERIENCE_SHARING = {
// Always private (never shared)
private: {
doorChoices: true, // What you chose
timePerDoor: true, // How long you spent
abandonedDoors: true, // Which you ran from
easterEggsFound: true, // Your discoveries
personalNotes: true // Future feature
},
// Can be shared (with consent)
shareable: {
doorsCompleted: true, // Which doors done
totalProgress: true, // Overall percentage
milestonesReached: true, // Achievements
visitCount: true, // How many returns
totalTime: true // Cumulative time
},
// Always public (for leaderboard)
public: {
completionRank: true, // Relative ranking
speedRunTimes: true, // Fastest completion
ngpCount: true // New Game+ count
}
};4.4 Comparison Messages
class ComparisonMessages {
static generateFriendComparison(myState, friendState) {
const comparison = FriendSystem.compareProgress(myState, friendState);
const messages = [];
// Progress comparison
if (comparison.aheadBy > 5) {
messages.push(`You're ${comparison.aheadBy} doors ahead.`);
} else if (comparison.aheadBy < -5) {
messages.push(`Your friend is ${Math.abs(comparison.aheadBy)} doors ahead.`);
}
// Unique discoveries
if (comparison.onlyFriends.length > 0) {
const randomDoor = comparison.onlyFriends[
Math.floor(Math.random() * comparison.onlyFriends.length)
];
const doorNum = parseInt(randomDoor.split('_')[1]);
messages.push(`Your friend found Door ${doorNum}. What did they see?`);
}
// Shared experiences
if (comparison.both.length > 0 && Math.random() > 0.5) {
messages.push(`You've both seen ${comparison.both.length} of the same doors.`);
}
// Mystery door
if (friendState.mysteryDoorSlot && !myState.doorStates[friendState.doorMapping[friendState.mysteryDoorSlot]]?.discovered) {
messages.push("Your friend has discovered something you haven't...");
}
return messages;
}
}5. VISITOR STATE TRACKING
5.1 Session Management
class SessionManager {
static startSession(state) {
const sessionId = VisitorIdentity.generateVisitorId();
const sessionStart = new Date().toISOString();
// Update state
const updatedState = {
...state,
lastVisit: sessionStart,
visitCount: (state.visitCount || 0) + 1,
sessionStartTime: sessionStart
};
// Set session cookies
CookieManager.set(COOKIE_NAMES.SESSION_ID, sessionId, { session: true });
CookieManager.set(COOKIE_NAMES.CURRENT_POSITION, state.currentPosition || 'hallway', { session: true });
return { state: updatedState, sessionId };
}
static endSession(state) {
const sessionEnd = Date.now();
const sessionStart = new Date(state.sessionStartTime).getTime();
const sessionDuration = sessionEnd - sessionStart;
return {
...state,
totalTimeInMotel: (state.totalTimeInMotel || 0) + sessionDuration
};
}
static isReturningVisitor(state) {
return (state.visitCount || 0) > 1;
}
static getTimeSinceLastVisit(state) {
if (!state.lastVisit) return null;
return Date.now() - new Date(state.lastVisit).getTime();
}
}5.2 Position Tracking
class PositionTracker {
static POSITIONS = {
HALLWAY: 'hallway',
DOOR_PREFIX: 'door_',
ENDING_PREFIX: 'ending_'
};
static setPosition(state, position) {
const updatedState = {
...state,
currentPosition: position
};
// Update session cookie for recovery
CookieManager.set(COOKIE_NAMES.CURRENT_POSITION, position, { session: true });
return updatedState;
}
static recordDoorVisit(state, doorId) {
const now = new Date().toISOString();
return {
...state,
currentPosition: `door_${doorId}`,
lastDoorVisited: doorId,
doorStates: {
...state.doorStates,
[doorId]: {
...state.doorStates[doorId],
discovered: true,
visited: true,
lastVisited: now
}
}
};
}
static recordDoorCompletion(state, doorId, choices = [], completionTime = 0) {
const doorState = state.doorStates[doorId] || {};
return {
...state,
doorsCompleted: [...new Set([...state.doorsCompleted, doorId])],
doorStates: {
...state.doorStates,
[doorId]: {
...doorState,
completed: true,
completionTime: (doorState.completionTime || 0) + completionTime,
choicesMade: [...(doorState.choicesMade || []), ...choices],
attempts: (doorState.attempts || 0) + 1
}
}
};
}
}5.3 Welcome Back Personalization
class WelcomeBackSystem {
static generateWelcomeMessage(state) {
const timeSince = SessionManager.getTimeSinceLastVisit(state);
const daysSince = timeSince ? Math.floor(timeSince / (1000 * 60 * 60 * 24)) : 0;
// Determine message category
let category = 'short';
if (daysSince > 365) category = 'year';
else if (daysSince > 30) category = 'month';
else if (daysSince > 7) category = 'week';
else if (daysSince > 1) category = 'days';
const messages = {
short: [
"Back so soon?",
"The motel didn't expect you yet.",
"You couldn't stay away."
],
days: [
`It's been ${daysSince} days. The motel has been waiting.`,
"The hallway remembers your footsteps.",
"Welcome back, wanderer."
],
week: [
"A week apart. The motel has missed you.",
"Seven days. The doors have shifted slightly.",
"You've been gone long enough to forget. But not long enough to escape."
],
month: [
"A month. The motel wondered if you'd return.",
"Thirty days of silence. And now, footsteps again.",
"You almost escaped. Almost."
],
year: [
"A year. The motel had begun to forget your face.",
"365 days. Some guests never return. You did.",
"The permanent guest returns. The motel remembers everything."
]
};
const categoryMessages = messages[category];
return categoryMessages[Math.floor(Math.random() * categoryMessages.length)];
}
static generateProgressSummary(state) {
const completed = state.doorsCompleted.length;
const total = 40;
const percent = Math.round((completed / total) * 100);
if (completed === 0) {
return "You stand at the beginning. Again.";
}
if (completed === total) {
return "You've seen everything. But have you truly checked out?";
}
const summaries = [
`${completed} of ${total} doors. ${percent}% of the motel knows you.`,
`You've opened ${completed} doors. ${total - completed} remain.`,
`${percent}% complete. The motel is ${percent}% yours.`,
`${completed} experiences. ${total - completed} more await.`
];
return summaries[Math.floor(Math.random() * summaries.length)];
}
static getRecommendedAction(state) {
const recommendation = RecommendationEngine.getRecommendedDoor(state);
if (!recommendation) {
return {
action: 'review',
message: "All doors await your return. Perhaps check your completed rooms?"
};
}
const doorNum = parseInt(recommendation.doorId.split('_')[1]);
return {
action: 'visit_door',
doorId: recommendation.doorId,
slot: recommendation.slot,
message: RecommendationEngine.getRecommendationMessage(recommendation, state)
};
}
}6. DYNAMIC CONTENT DELIVERY
6.1 Door Content Variants
class DynamicContentEngine {
// Each door can have multiple variants based on visitor state
static getDoorVariant(doorId, state) {
const doorNum = parseInt(doorId.split('_')[1]);
const variants = this.getAvailableVariants(doorId);
// Select variant based on visitor hash and progress
const rng = VisitorIdentity.seededRandom(state.visitorHash + doorId);
const variantIndex = Math.floor(rng() * variants.length);
// But also consider progress for some doors
if (doorNum % 5 === 0) { // Every 5th door adapts
return this.getAdaptiveVariant(doorId, state, variants);
}
return variants[variantIndex];
}
static getAvailableVariants(doorId) {
// Each door has base + variants
const baseVariants = ['default', 'intense', 'subtle', 'alternate'];
// Some doors have special variants
const specialVariants = {
'door_13': ['default', 'intense', 'subtle', 'alternate', 'meta'],
'door_23': ['default', 'intense', 'subtle', 'alternate', 'choice_heavy'],
'door_40': ['default', 'intense', 'subtle', 'alternate', 'true_ending', 'secret_ending']
};
return specialVariants[doorId] || baseVariants;
}
static getAdaptiveVariant(doorId, state, variants) {
const completed = state.doorsCompleted.length;
const abandoned = state.doorsAbandoned.length || 0;
// Adapt based on play style
if (abandoned > completed * 0.5) {
// Player abandons often - use subtle variant
return variants.find(v => v === 'subtle') || variants[0];
}
if (completed > 20) {
// Experienced player - can handle intense
return variants.find(v => v === 'intense') || variants[0];
}
// Default selection
const rng = VisitorIdentity.seededRandom(state.visitorHash + doorId);
return variants[Math.floor(rng() * variants.length)];
}
}6.2 Adaptive Difficulty
class AdaptiveDifficulty {
static calculateDifficultyModifier(state) {
const modifiers = {
base: 1.0,
experience: 0,
completionRate: 0,
timeModifier: 0,
preference: 0
};
// Experience modifier (more doors = higher base difficulty acceptable)
const completed = state.doorsCompleted.length;
modifiers.experience = Math.min(completed * 0.02, 0.4); // Max +0.4
// Completion rate (if they complete most doors, they can handle more)
const totalAttempts = Object.values(state.doorStates)
.reduce((sum, d) => sum + (d.attempts || 0), 0);
const completionRate = completed / Math.max(totalAttempts, 1);
modifiers.completionRate = (completionRate - 0.5) * 0.2; // -0.1 to +0.1
// Time modifier (fast completers get harder content)
const avgTime = state.totalTimeInMotel / Math.max(completed, 1);
if (avgTime < 5 * 60 * 1000) { // Less than 5 min per door
modifiers.timeModifier = 0.1;
}
// User preference (if set)
if (state.settings?.difficulty) {
modifiers.preference = {
'easy': -0.2,
'normal': 0,
'hard': 0.2
}[state.settings.difficulty] || 0;
}
const totalModifier = Object.values(modifiers).reduce((a, b) => a + b, 0);
return Math.max(0.5, Math.min(totalModifier, 2.0)); // Clamp 0.5x to 2x
}
static applyDifficulty(content, modifier) {
return {
...content,
intensity: content.intensity * modifier,
duration: content.duration / modifier, // Harder = faster
choiceComplexity: Math.min(content.choiceComplexity * modifier, 5)
};
}
}6.3 Story Branching
class StoryBranching {
// Track choices across doors for narrative continuity
static recordChoice(state, doorId, choice) {
const choiceKey = `${doorId}:${choice}`;
return {
...state,
globalChoices: [...(state.globalChoices || []), choiceKey]
};
}
// Get narrative state based on all choices
static getNarrativeState(state) {
const choices = state.globalChoices || [];
return {
// Has the visitor generally chosen fight or flight?
tendency: this.calculateTendency(choices),
// Have they shown curiosity or avoidance?
curiosity: this.calculateCuriosity(choices),
// Have they completed doors quickly or slowly?
pace: this.calculatePace(state),
// Overall narrative path
path: this.determinePath(choices)
};
}
static calculateTendency(choices) {
const fight = choices.filter(c => c.includes('confront')).length;
const flight = choices.filter(c => c.includes('avoid')).length;
if (fight > flight * 1.5) return 'confrontational';
if (flight > fight * 1.5) return 'avoidant';
return 'balanced';
}
static calculateCuriosity(choices) {
const curious = choices.filter(c =>
c.includes('investigate') || c.includes('explore')
).length;
return curious > choices.length * 0.3 ? 'high' : 'low';
}
static calculatePace(state) {
const avgTime = state.totalTimeInMotel / Math.max(state.doorsCompleted.length, 1);
if (avgTime < 3 * 60 * 1000) return 'fast';
if (avgTime > 10 * 60 * 1000) return 'slow';
return 'normal';
}
static determinePath(choices) {
// Complex path determination based on choice patterns
// Returns: 'seeker', 'survivor', 'observer', 'participant'
// Implementation depends on specific choice taxonomy
return 'seeker'; // Default
}
// Modify door content based on narrative state
static adaptContentForNarrative(content, narrativeState) {
const adaptations = {
confrontational: {
dialogue: 'direct',
options: 'action_oriented'
},
avoidant: {
dialogue: 'subtle',
options: 'escape_focused'
},
high_curiosity: {
reveals: 'extra_lore',
secrets: 'more_visible'
},
fast_pace: {
pacing: 'quicker',
fluff: 'reduced'
}
};
// Apply relevant adaptations
let adapted = { ...content };
if (adaptations[narrativeState.tendency]) {
adapted = { ...adapted, ...adaptations[narrativeState.tendency] };
}
return adapted;
}
}6.4 Easter Egg System
class EasterEggSystem {
static EASTER_EGGS = {
'secret_message_01': {
condition: (state) => state.doorsCompleted.includes('door_07') && state.doorsCompleted.includes('door_13'),
hint: "Some doors speak to each other.",
reward: 'lore_fragment_01'
},
'hidden_door_01': {
condition: (state) => state.totalTimeInMotel > 60 * 60 * 1000, // 1 hour total
hint: "Patience reveals what haste conceals.",
reward: 'bonus_door_access'
},
'speed_demon': {
condition: (state) => state.doorsCompleted.length >= 10 &&
state.totalTimeInMotel < 30 * 60 * 1000,
hint: "The motel respects those who don't linger.",
reward: 'speed_badge'
},
'completionist_secret': {
condition: (state) => state.doorsCompleted.length === 40 &&
Object.values(state.doorStates).every(d => d.choicesMade?.length > 0),
hint: "Every choice matters. Every door holds more than one truth.",
reward: 'true_ending_access'
},
'return_visitor_bonus': {
condition: (state) => state.visitCount >= 10,
hint: "The motel remembers those who keep returning.",
reward: 'insider_knowledge'
}
};
static checkEasterEggs(state) {
const newlyUnlocked = [];
Object.entries(this.EASTER_EGGS).forEach(([eggId, egg]) => {
if (!state.easterEggsFound?.includes(eggId) && egg.condition(state)) {
newlyUnlocked.push({
id: eggId,
...egg
});
}
});
return newlyUnlocked;
}
static unlockEasterEgg(state, eggId) {
return {
...state,
easterEggsFound: [...(state.easterEggsFound || []), eggId]
};
}
}7. TECHNICAL IMPLEMENTATION
7.1 Cookie Manager
class CookieManager {
static set(name, value, options = {}) {
const defaults = {
days: 365,
secure: true,
sameSite: 'Lax',
path: '/'
};
const opts = { ...defaults, ...options };
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (opts.days && !opts.session) {
const date = new Date();
date.setTime(date.getTime() + (opts.days * 24 * 60 * 60 * 1000));
cookieString += `; expires=${date.toUTCString()}`;
}
if (opts.secure) cookieString += '; secure';
if (opts.sameSite) cookieString += `; samesite=${opts.sameSite}`;
if (opts.path) cookieString += `; path=${opts.path}`;
document.cookie = cookieString;
}
static get(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.trim().split('=');
if (decodeURIComponent(cookieName) === name) {
return decodeURIComponent(cookieValue);
}
}
return null;
}
static delete(name) {
this.set(name, '', { days: -1 });
}
static exists(name) {
return this.get(name) !== null;
}
}7.2 State Manager
class MotelStateManager {
constructor() {
this.state = null;
this.listeners = [];
this.saveDebounce = null;
}
// Initialize or load existing state
async init() {
// Try to load from cookies
const savedState = CookieManager.get(COOKIE_NAMES.PRIMARY_STATE);
if (savedState) {
try {
this.state = StateCompressor.decompress(savedState);
this.state = DataLifecycleManager.checkVersion(this.state);
// Check for cleanup
if (DataLifecycleManager.shouldCleanup(this.state)) {
console.log('Motel: State expired, starting fresh');
this.state = this.createNewState();
}
} catch (e) {
console.error('Motel: Failed to load state, creating new');
this.state = this.createNewState();
}
} else {
this.state = this.createNewState();
}
// Start session
const { state: updatedState } = SessionManager.startSession(this.state);
this.state = updatedState;
// Save initial state
this.saveState();
return this.state;
}
createNewState() {
const visitorId = VisitorIdentity.generateVisitorId();
const visitorHash = VisitorIdentity.generateVisitorHash(visitorId);
return {
visitorId,
visitorHash,
firstVisit: new Date().toISOString(),
lastVisit: new Date().toISOString(),
visitCount: 0,
totalProgress: 0,
doorsUnlocked: ['slot_01'],
doorsCompleted: [],
doorsAbandoned: [],
doorMapping: DoorMappingGenerator.generateMapping(visitorHash),
mysteryDoorSlot: null, // Will be set after generation
doorStates: {},
currentPosition: 'hallway',
lastDoorVisited: null,
sessionStartTime: new Date().toISOString(),
totalTimeInMotel: 0,
milestonesReached: [],
easterEggsFound: [],
newGamePlusUnlocked: false,
settings: {
audioEnabled: true,
reducedMotion: false,
highContrast: false
},
schemaVersion: '1.0.0',
lastMigrated: new Date().toISOString()
};
}
// Update state with change
updateState(updater) {
const newState = updater(this.state);
this.state = newState;
// Notify listeners
this.listeners.forEach(listener => listener(newState));
// Debounced save
clearTimeout(this.saveDebounce);
this.saveDebounce = setTimeout(() => this.saveState(), 500);
return newState;
}
// Save state to cookies
saveState() {
if (!this.state) return;
const compressed = StateCompressor.compress(this.state);
CookieManager.set(COOKIE_NAMES.PRIMARY_STATE, compressed);
// Update lightweight progress cookie
CookieManager.set(
COOKIE_NAMES.PROGRESS_PERCENT,
Math.round(this.state.totalProgress * 100).toString(),
{ days: 365 }
);
// Mark as returning visitor
if (this.state.visitCount > 1) {
CookieManager.set(COOKIE_NAMES.RETURNING_VISITOR, 'true', { days: 365 });
}
// Sync to localStorage for backup
try {
localStorage.setItem(
STORAGE_CONFIG.localStorage.key,
JSON.stringify({
doorStates: this.state.doorStates,
easterEggsFound: this.state.easterEggsFound,
lastSaved: new Date().toISOString()
})
);
} catch (e) {
// localStorage might be full or unavailable
}
}
// Get current state
getState() {
return this.state;
}
// Subscribe to state changes
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
// End session and cleanup
endSession() {
this.state = SessionManager.endSession(this.state);
this.saveState();
}
}
// Singleton instance
const motelState = new MotelStateManager();7.3 Tab Synchronization
class TabSyncManager {
constructor(stateManager) {
this.stateManager = stateManager;
this.channel = new BroadcastChannel('motel_sync');
this.channel.onmessage = (event) => {
this.handleSyncMessage(event.data);
};
// Listen for storage events (for browsers without BroadcastChannel)
window.addEventListener('storage', (e) => {
if (e.key === STORAGE_CONFIG.localStorage.key) {
this.handleStorageChange(e.newValue);
}
});
}
handleSyncMessage(message) {
switch(message.type) {
case 'STATE_UPDATED':
// Another tab updated state
this.stateManager.state = message.payload;
break;
case 'DOOR_OPENED':
// Someone opened a door in another tab
console.log('Motel: Door opened in another tab');
break;
case 'PROGRESS_MADE':
// Progress in another tab
this.stateManager.state = {
...this.stateManager.state,
...message.payload
};
break;
}
}
handleStorageChange(newValue) {
try {
const data = JSON.parse(newValue);
// Merge localStorage data with current state
this.stateManager.state = {
...this.stateManager.state,
doorStates: { ...this.stateManager.state.doorStates, ...data.doorStates },
easterEggsFound: [...new Set([
...this.stateManager.state.easterEggsFound,
...(data.easterEggsFound || [])
])]
};
} catch (e) {
console.error('Motel: Failed to sync from storage');
}
}
// Broadcast state change to other tabs
broadcast(type, payload) {
this.channel.postMessage({ type, payload, timestamp: Date.now() });
}
// Notify other tabs of door interaction
notifyDoorInteraction(doorId, action) {
this.broadcast('DOOR_INTERACTION', { doorId, action });
}
}7.4 Data Versioning
const SCHEMA_VERSIONS = {
'1.0.0': {
fields: [
'visitorId', 'visitorHash', 'firstVisit', 'lastVisit', 'visitCount',
'totalProgress', 'doorsUnlocked', 'doorsCompleted', 'doorsAbandoned',
'doorMapping', 'mysteryDoorSlot', 'doorStates', 'currentPosition',
'lastDoorVisited', 'sessionStartTime', 'totalTimeInMotel',
'milestonesReached', 'easterEggsFound', 'newGamePlusUnlocked',
'settings', 'schemaVersion', 'lastMigrated'
],
migrations: {}
}
};
class SchemaValidator {
static validate(state) {
const version = state.schemaVersion || '0.0.0';
const schema = SCHEMA_VERSIONS[version];
if (!schema) {
console.warn(`Motel: Unknown schema version ${version}`);
return false;
}
// Check required fields
const missing = schema.fields.filter(f => !(f in state));
if (missing.length > 0) {
console.warn(`Motel: Missing fields: ${missing.join(', ')}`);
return false;
}
return true;
}
static migrate(state, targetVersion) {
const currentVersion = state.schemaVersion || '0.0.0';
if (currentVersion === targetVersion) {
return state;
}
// Apply migrations in sequence
// This would be expanded for future versions
console.log(`Motel: Migrating from ${currentVersion} to ${targetVersion}`);
return {
...state,
schemaVersion: targetVersion,
lastMigrated: new Date().toISOString()
};
}
}8. ANALYTICS
8.1 Anonymous Tracking
class MotelAnalytics {
constructor() {
this.enabled = false;
this.queue = [];
}
init() {
// Check consent
this.enabled = CookieManager.get(COOKIE_NAMES.ANALYTICS_CONSENT) === 'true';
}
// Track event (anonymous)
track(eventName, properties = {}) {
if (!this.enabled) return;
const event = {
name: eventName,
properties: {
...properties,
timestamp: Date.now(),
sessionId: CookieManager.get(COOKIE_NAMES.SESSION_ID)
},
// No visitor ID - completely anonymous
};
this.queue.push(event);
// Flush queue periodically
if (this.queue.length >= 10) {
this.flush();
}
}
// Predefined events
trackDoorEnter(doorId) {
this.track('door_enter', { doorId });
}
trackDoorComplete(doorId, duration) {
this.track('door_complete', { doorId, duration });
}
trackDoorAbandon(doorId, timeSpent) {
this.track('door_abandon', { doorId, timeSpent });
}
trackMilestone(milestoneId) {
this.track('milestone_reached', { milestoneId });
}
trackEasterEgg(eggId) {
this.track('easter_egg_found', { eggId });
}
trackSessionEnd(duration, doorsCompleted) {
this.track('session_end', { duration, doorsCompleted });
}
// Send to analytics endpoint
flush() {
if (this.queue.length === 0) return;
const events = [...this.queue];
this.queue = [];
// Send to your analytics endpoint
fetch('/api/motel/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events }),
keepalive: true
}).catch(() => {
// Re-queue on failure
this.queue.unshift(...events);
});
}
}
const motelAnalytics = new MotelAnalytics();8.2 Key Metrics to Track
const ANALYTICS_METRICS = {
// Door popularity
doorMetrics: {
enterCount: 'How many times each door is entered',
completionRate: 'Percentage who complete vs abandon',
avgTime: 'Average time spent in each door',
dropOffPoint: 'Where in the door people leave'
},
// Progress metrics
progressMetrics: {
completionRate: 'How many visitors complete all doors',
avgDoorsPerSession: 'Average doors completed per visit',
returnRate: 'How many visitors come back',
timeToComplete: 'Total time to complete all doors'
},
// Engagement metrics
engagementMetrics: {
sessionDuration: 'How long people stay per session',
doorsPerSession: 'How many doors per visit',
returnFrequency: 'How often people return',
ngpAdoption: 'How many start New Game+'
},
// Experience metrics
experienceMetrics: {
mysteryDoorDiscovery: 'How many find the mystery door',
easterEggDiscovery: 'Which easter eggs are found most',
variantSelection: 'Which door variants are experienced',
endingReached: 'Which endings are discovered'
}
};8.3 Dashboard Data Structure
// Data structure for analytics dashboard
const DASHBOARD_DATA = {
overview: {
totalSessions: 0,
uniqueVisitors: 0, // Anonymous count
completionRate: 0,
avgSessionDuration: 0
},
doors: {
// Per-door stats
'door_01': {
enters: 0,
completes: 0,
abandons: 0,
avgTime: 0,
completionRate: 0
}
// ... for all 40 doors
},
progression: {
dropOffPoints: [], // Where people stop
milestoneReachRates: {}, // How many reach each milestone
ngpStarts: 0
},
temporal: {
sessionsByHour: [],
sessionsByDay: [],
completionTimeDistribution: []
}
};9. IMPLEMENTATION CHECKLIST
Phase 1: Core State Management
- Implement CookieManager
- Implement StateCompressor
- Implement MotelStateManager
- Create visitor ID generation
- Set up cookie consent banner
Phase 2: Progression System
- Implement door unlock logic
- Create progress calculation
- Build milestone system
- Add New Game+ functionality
Phase 3: Personalization
- Implement door mapping generator
- Create mystery door system
- Build recommendation engine
- Add door visibility logic
Phase 4: Friend System
- Generate friend codes
- Implement comparison logic
- Create differentiated mappings
- Add comparison messages
Phase 5: Dynamic Content
- Build content variant system
- Implement adaptive difficulty
- Create story branching
- Add easter egg system
Phase 6: Polish
- Add tab synchronization
- Implement analytics
- Create welcome back system
- Add data migration
10. API REFERENCE
Quick Start
// Initialize the motel
const state = await motelState.init();
// Check if returning visitor
const isReturning = SessionManager.isReturningVisitor(state);
// Get welcome message
const welcome = WelcomeBackSystem.generateWelcomeMessage(state);
// Get recommended door
const recommendation = RecommendationEngine.getRecommendedDoor(state);
// Enter a door
motelState.updateState(s => PositionTracker.recordDoorVisit(s, 'door_07'));
// Complete a door
motelState.updateState(s => PositionTracker.recordDoorCompletion(s, 'door_07', ['choice_a'], 120000));
// Check for unlocks
const newUnlocks = PROGRESSION_RULES.unlockRules.filter(r => r.trigger(state));
// Check for easter eggs
const newEggs = EasterEggSystem.checkEasterEggs(state);Appendix: Door ID Reference
door_01 - door_10 : Entry-level existential threats
door_11 - door_20 : Intermediate experiences
door_21 - door_30 : Advanced encounters
door_31 - door_39 : Expert-level challenges
door_40 : Final door / True ending
Document Version: 1.0.0 Last Updated: 2024 THE MOTEL remembers its guests.