Security Shift Blueprint, Build Five Nights at Freddy's
JVDesignStudio - https://jvdesignstudio.co.uk/workshops/fnaf-blueprint.html

Offline reference pack for this workshop.
This is a companion copy of the build steps, so you can follow along without
the website. Open the workshop online for the interactive screenshots, videos
and the copy buttons.

What you build: Security Shift Blueprint, Build Five Nights at Freddy's

--------------------------------------------------------------------
STEP-BY-STEP BUILD
--------------------------------------------------------------------

## STEP 01 · VARIABLES - Global State Variables
- Before any game logic can run, we need variables, the game's source of truth. Every mechanic (power bar, timer, door) reads from and writes to these. Wire the variable declarations to the BeginPlay event.

## STEP 02 · GAME LOOP - Event BeginPlay',x:6,y:34,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vset',title:'Set playerPower',sub:'Float',x:185,y:14,w:160,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'float',label:'100.0'}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'vset',title:'Set gameRunning',sub:'Bool',x:185,y:90,w:160,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'bool',label:'True'}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'vset',title:'Set gameTimeMins',sub:'Int',x:370,y:14,w:160,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'int',label:'0'}],rp:[{id:'r4',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l4e'},{f:'r2',t:'l3e'}],[]), boxes:[ {t:'box-c',tx:'Variables are the "truth" of your game. The visual power bar, the door state, the clock, they\'re all just reflections of these numbers. Change the variable, update the display. Never the other way around.'}, {t:'box-t',tx:'playerPower starts at 100. gameRunning starts true. gameTimeMins starts at 0 (midnight). These three variables govern everything else in the game.'}, ], quiz:{q:"What does a variable store in a game?",opts:["A picture","A value the game reads and writes","A sound effect"],correct:1}, onApply(){G.f_power=true;G.phase='playing';draw();sout('✅ Variables declared, power, timer, running state all initialised!');updateInfoCard();} }, {id:'s02
- A game with no clock is just a static page. setInterval() makes a function repeat every X milliseconds, that repetition IS the game loop. Wire BeginPlay ? setInterval ? gameLoop. Set the tick speed in milliseconds.

## STEP 03 · DRAIN - Event BeginPlay',x:4,y:36,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'setInterval',sub:'Async Timer',x:180,y:12,w:168,lp:[{id:'l2e',type:'exec',label:''},{id:'l2f',type:'func',label:'Function'},{id:'l2d',type:'int',label:'ms Delay'}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2id',type:'int',label:'Interval ID'}],vf:{hint:'Tick speed ms (e.g. 2000)',val:''}}, {id:'n3',type:'vset',title:'Store Interval ID',sub:'→ gameIntervalId',x:370,y:18,w:155,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'int',label:'ID'}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2id',t:'l3v'}],[{id:'n2',ok:v=>parseInt(v)>=500&&parseInt(v)<=5000}]), boxes:[ {t:'box-c',tx:'setInterval returns an ID number. You MUST store it. Without that ID, you cannot call clearInterval() later, and then the loop runs forever even after Game Over. That is a memory leak bug.'}, {t:'box-t',tx:'2000ms (2 seconds) is a good starting tick. Shorter ticks = faster nights and harder gameplay. Longer ticks = more tense, slower drain. The student decides the difficulty here.'}, {t:'box-n',tx:'Why store the ID? Think of it like a ticket. setInterval issues you a ticket. clearInterval needs that ticket back to cancel the booking. Lose the ticket, the clock never stops.'}, ], quiz:{q:"What does setInterval return?",opts:["A boolean","An ID number you need to stop it later","The function itself"],correct:1}, onApply(v){NIGHT.tickMs=parseInt(v['n2'])||2000;G.f_loop=true;initNight();updateInfoCard();sout('✅ Game loop started at '+NIGHT.tickMs+'ms per tick. The night has begun!');} }, {id:'s03
- Every tick, a small amount of power silently disappears. You don't press anything, it just drains. This is the passive drain. Wire Event Tick ? Subtract ? Clamp ? Set playerPower. Type the drain amount per tick.

## STEP 04 · DISPLAY - On Game Tick',sub:'setInterval callback',x:4,y:34,w:168,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'math',title:'Subtract Drain',sub:'playerPower - drain',x:195,y:10,w:155,lp:[{id:'l2a',type:'float',label:'Power'},{id:'l2b',type:'float',label:'Drain'}],rp:[{id:'r2',type:'float',label:'Result'}],vf:{hint:'Drain per tick (e.g. 0.4)',val:''}}, {id:'n3',type:'func',title:'Clamp (0, 100)',x:370,y:4,w:148,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'float',label:'Value'}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3v',type:'float',label:'Clamped'}]}, {id:'n4',type:'vset',title:'Set playerPower',x:370,y:70,w:150,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'float',label:'Value'}]}, ],[{f:'r1',t:'l3e'},{f:'r2',t:'l3v'},{f:'r3e',t:'l4e'},{f:'r3v',t:'l4v'}],[{id:'n2',ok:v=>parseFloat(v)>0&&parseFloat(v)<=2}]), boxes:[ {t:'box-c',tx:'0.4 per tick at 2000ms = roughly 3.5 minutes of real time until power dies at default settings. This is intentional, the student must be active to survive. Pure idling = game over.'}, {t:'box-t',tx:'Why Clamp? Without it, playerPower could go below 0, to -12, -50, etc. Your game-over check (playerPower <= 0) might trigger slightly after the bar is already at zero. Clamping makes the check reliable.'}, {t:'box-u',tx:'0.2 = easy (5+ mins power). 0.4 = medium (3.5 mins). 0.8 = hard (1.5 mins). 1.5 = brutally short. Let the student experiment, this is their game balance choice.'}, ], quiz:{q:"Why do we clamp playerPower between 0 and 100?",opts:["To make the game harder","To prevent the value going below 0 or above 100","To speed up the game"],correct:1}, onApply(v){NIGHT.passiveDrain=parseFloat(v['n2'])||0.4;G.f_power=true;updateInfoCard();sout('✅ Passive drain set to '+NIGHT.passiveDrain+'/tick. Watch the power bar drop!');} }, {id:'s04
- State and appearance are separate. playerPower is the truth. The power bar is just a reflection of it. Wire On Tick ? Read playerPower ? Calculate bar width ? Set style. This separation is the most important concept in game architecture.

## STEP 05 · LOSE STATE - On Game Tick',x:4,y:36,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vget',title:'Get playerPower',x:180,y:14,w:148,rp:[{id:'r2',type:'float',label:'Power %'}]}, {id:'n3',type:'func',title:'Set Bar Width',sub:'power + "%"',x:352,y:4,w:160,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'float',label:'Power'}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Set Bar Colour',sub:'Green/Orange/Red',x:352,y:66,w:160,lp:[{id:'l4e',type:'exec',label:''},{id:'l4v',type:'float',label:'Power'}]}, ],[{f:'r1',t:'l3e'},{f:'r1',t:'l4e'},{f:'r2',t:'l3v'},{f:'r2',t:'l4v'}],[]), boxes:[ {t:'box-c',tx:'Rule: the variable (playerPower) is written first. The display reads it. You NEVER change playerPower by adjusting the bar, that would be like moving a speedometer needle to change your car\'s speed.'}, {t:'box-t',tx:'Colour thresholds: >60% = teal (safe), >30% = orange (warning), =30% = red (critical). The student sees the danger building visually. This is called "affordance", the interface communicates state without words.'}, ], quiz:{q:"Should you change playerPower by adjusting the display bar?",opts:["Yes, the bar controls the variable","No, the variable is the truth, the bar reflects it","It doesn't matter"],correct:1}, onApply(){draw();sout('✅ Display wired! Power bar now updates every tick. Watch the colour shift as power drops.');} }, {id:'s05
- If power hits zero, everything stops. The loop stops. The buttons disable. The message shows. Wire On Tick ? Branch (playerPower = 0) ? True ? clearInterval ? showGameOver.

## STEP 06 · LIGHT - On Game Tick',x:4,y:36,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'playerPower = 0?',x:180,y:10,w:148,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Condition'}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'func',title:'clearInterval',sub:'Stop the clock',x:352,y:4,w:152,lp:[{id:'l3e',type:'exec',label:''},{id:'l3id',type:'int',label:'Interval ID'}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Show Game Over',sub:'Disable all inputs',x:352,y:64,w:152,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'The Branch node is like an if/else. True path = game over. False path = continue the tick normally. Without this check, power would go negative forever and the game would never end.'}, {t:'box-n',tx:'CRITICAL: clearInterval MUST be called here. If you skip it, the tick function keeps firing even with playerPower at -200. The game visually shows "over" but logic is still running in the background, a silent memory leak.'}, {t:'box-t',tx:'Disable all buttons in the game over function. If the player can still click after death, they might accidentally modify state variables in an inconsistent game-over state.'}, ], quiz:{q:"What happens if you forget to call clearInterval on game over?",opts:["Nothing","The game loop keeps running in the background","The game restarts"],correct:1}, onApply(){sout('✅ Game over condition wired! Zero power = GAME OVER. Try draining the power in the preview.');} }, /* ----------- MODULE 2, ACTIONS ----------- */ {id:'s06
- Clicking "Light On" costs power. Before subtracting the cost, we check if there's enough power first. If there isn't, the action fails gracefully. Wire Button Click ? Branch (power = cost) ? True ? Subtract Cost ? Update.

## STEP 07 · CAMERAS - On Light Button Click',x:4,y:34,w:175,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'playerPower = LIGHT_COST?',x:202,y:10,w:168,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Enough power?'}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'func',title:'Subtract Light Cost',sub:'playerPower -= 15',x:390,y:4,w:140,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Show "Not enough"',sub:'Alert message',x:390,y:62,w:140,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'This pattern, check, then act, is called "defensive coding". You never assume. Always validate the precondition before executing. Without the check, a player could spam the button and drive power to -500.'}, {t:'box-u',tx:'LIGHT_COST is a constant (15%). It never changes at runtime, it\'s a rule of the game. Treat action costs as constants so you can balance the whole game by changing one number.'}, {t:'box-w',tx:'Your turn: Click the 💡 LIGHT button in the preview. Watch the power drop by 15%. Click it again, it turns off. Actions are toggles with a cost on the first press.'}, ], quiz:{q:"Why check if there's enough power BEFORE subtracting the cost?",opts:["It's faster","To prevent power going negative from spam-clicking","It looks better"],correct:1}, onApply(){G.f_actions=true;draw();sout('? Light action wired! ? ? Click the light button in the office preview.');} }, {id:'s07
- The camera feed shows where the animatronic is, but activating it costs power. Wire the Camera button ? Check power = camCost ? Subtract cost ? Toggle camActive ? Refresh display.

## STEP 08 · DOOR - On Camera Button Click',x:4,y:36,w:172,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'playerPower = CAM_COST?',x:198,y:12,w:168,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Check'}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'vset',title:'Subtract Cam Cost',sub:'playerPower -= 5',x:388,y:4,w:145,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'vset',title:'Toggle camActive',sub:'!camActive',x:388,y:62,w:145,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'Cameras are cheap (5% cost) compared to the door (10%) and light (15%). They\'re meant to be used frequently to track the animatronic. The risk/reward ratio is intentional, checking too often drains power, not checking is dangerous.'}, {t:'box-s',tx:'The camActive boolean controls what the left monitor in the office displays. When true, it shows a "LIVE FEED" display. When false, just static. The visual changes; the animatronic\'s position doesn\'t.'}, {t:'box-t',tx:'Real FNaF uses a camera map with multiple rooms. Our version has Cam 1 (hallway). Later you could extend to Cam 2, Cam 3, each a different room, each costing slightly different amounts of power.'}, ], quiz:{q:"Why are cameras cheaper (5%) than the light (15%)?",opts:["Bug in the code","Cameras are meant to be used frequently for information","They do less"],correct:1}, onApply(){G.f_cams=true;draw();sout('? Camera system wired! 📷 The left monitor now shows a live feed when active.');} }, {id:'s08
- Closing the door blocks the animatronic, but it costs power every tick while it's sealed. Wire Door button ? Toggle doorClosed ? Add passive door drain to the game loop.

## STEP 09 🏷️ NAME - On Door Button Click',x:4,y:36,w:165,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'doorClosed already?',x:192,y:10,w:162,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'doorClosed'}],rp:[{id:'r2t',type:'exec',label:'Open it'},{id:'r2f',type:'exec',label:'Close it'}]}, {id:'n3',type:'vset',title:'Set doorClosed False',x:376,y:4,w:152,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'vset',title:'Set doorClosed True',sub:'cost: 10% to close',x:376,y:62,w:152,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'The door has no cost to open, only to close (10% to slam it shut) and an ongoing drain while it stays closed. This creates the core tension: close the door early and drain fast, or wait until the animatronic is near?'}, {t:'box-n',tx:'The game loop now has: passiveDrain + (doorClosed ? doorPassiveDrain : 0) + (lightOn ? lightPassiveDrain : 0). Three drain sources compete. The student must learn to manage all three at once.'}, {t:'box-w',tx:'Try the strategy: let the animatronic reach the hallway dot, THEN slam the door. This is the intended risk/reward loop, timing the door is the skill expression in the game.'}, ], quiz:{q:"When does the door drain extra power?",opts:["Only when you close it","Every tick while it stays closed","When the animatronic is near"],correct:1}, onApply(){G.f_door=true;draw();sout('? Door system live! 🚪 The door drains extra power while closed, but blocks the animatronic.');} }, /* ----------- MODULE 3, NIGHT TIMER ----------- */ {id:'s09
- Before we build the AI, give your animatronic a name. This shows in warnings and the game over screen. Wire On BeginPlay ? Set animatronicName ? Apply to HUD. Type the name.

## STEP 10 ⚡ SPEED - On BeginPlay',x:8,y:36,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'anim',title:'Set Anim Name',sub:'Night Setup',x:178,y:14,w:162,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'string',label:'Name'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Animatronic name',val:''}}, {id:'n3',type:'func',title:'Apply to HUD',x:362,y:14,w:148,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[{id:'n2',ok:v=>v&&v.trim().length>=2}]), boxes:[ {t:'box-n',tx:'This is the student\'s creative decision. Freddy, Bonnie, Chica, Foxy, or something completely original. The name appears in warning messages ("Bonnie is at the door!") which massively increases tension.'}, {t:'box-t',tx:'Naming your antagonist is a fundamental UX decision. Named enemies feel more threatening than unnamed ones. This is why FNaF\'s characters each have distinct names and personalities.'}, ], quiz:{q:"Why does naming the animatronic increase tension?",opts:["It doesn't","Named enemies feel more threatening than unnamed ones","It changes the AI"],correct:1}, onApply(v){NIGHT.animName=v['n2']||'Freddy';updateInfoCard();sout('✅ Animatronic named: '+NIGHT.animName+'. It\'ll appear in all warnings now.');} }, {id:'s10
- How aggressive is your animatronic? Type a probability value (0.0 to 1.0). Each tick, it rolls a random number, if the roll is below your value, it moves one step closer.

## STEP 11 🔥 AGGRO - On BeginPlay',x:8,y:36,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'anim',title:'Set Move Chance',sub:'Night Setup',x:178,y:12,w:165,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'float',label:'0.0-1.0'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Move chance 0.1-0.5',val:''}}, {id:'n3',type:'func',title:'Apply AI System',x:365,y:12,w:155,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[{id:'n2',ok:v=>parseFloat(v)>=0.05&&parseFloat(v)<=0.8}]), boxes:[ {t:'box-c',tx:'0.1 = moves infrequently, easy night. 0.25 = moderate threat. 0.5 = aggressive. 0.8 = nearly every tick it advances, almost unbeatable without perfect door management.'}, {t:'box-n',tx:'The real FNaF uses an AI level 0-20 per animatronic, where higher levels increase this probability. Night 1 starts low. Night 5 cranks every animatronic to max. The probability is the entire difficulty engine.'}, {t:'box-t',tx:'Probability-based AI feels more alive than scripted AI because it\'s unpredictable. Scripted AI ("moves every 10 ticks exactly") is easy to memorise. Probability AI keeps the player uncertain.'}, ], quiz:{q:"What does a move chance of 0.5 mean?",opts:["Moves every 0.5 seconds","50% chance of moving each tick","Moves half as fast"],correct:1}, onApply(v){NIGHT.animMoveChance=parseFloat(v['n2'])||0.2;updateInfoCard();sout('✅ Move chance set to '+NIGHT.animMoveChance+'. Higher = more aggressive.');} }, {id:'s11
- When power is low, the animatronic gets bolder. Wire an additional probability bonus that activates below the warning threshold. This is what makes the final 20% of power so terrifying.

## STEP 12 · AI SYSTEM - On AI Tick',x:6,y:36,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'playerPower < warnThreshold?',x:178,y:10,w:185,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Low power?'}],rp:[{id:'r2t',type:'exec',label:'Bonus'},{id:'r2f',type:'exec',label:'Normal'}]}, {id:'n3',type:'func',title:'Add Aggro Bonus',sub:'chance += 0.35',x:386,y:4,w:148,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Roll Move Check',x:386,y:62,w:148,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'},{f:'r2f',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'This is the genius of FNaF\'s design loop: the thing that drains your power (the animatronic) becomes MORE dangerous when your power is low. Low power = higher drain AND higher threat. Two systems feeding the same spiral.'}, {t:'box-s',tx:'The aggression bonus (0.35) stacks on top of the base move chance. If base chance is 0.2 and power is below threshold, effective chance becomes 0.55. That\'s more than double, the player can feel the shift.'}, ], quiz:{q:"What makes FNaF's low-power phase so scary?",opts:["Louder sounds","The animatronic gets faster AND power drains faster","The screen goes dark"],correct:1}, onApply(){sout('✅ Low-power aggression bonus wired! The animatronic gets faster when you\'re below '+NIGHT.warnThreshold+'%.');} }, /* ----------- MODULE 4, ANIMATRONIC AI ----------- */ {id:'s12
- The animatronic has 4 positions: 0=Far, 1=Hallway, 2=Door, 3=Inside. Moving to position 3 triggers game over. Wire the position variable and draw the tracker dots in the HUD.

## STEP 13 · MOVE LOGIC - On BeginPlay',x:6,y:36,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vset',title:'Set animPos = 0',sub:'Start position: Far',x:176,y:14,w:162,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'int',label:'0'}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'func',title:'Init Position Tracker',sub:'Draw HUD dots FAR/HALL/DOOR/!!!',x:360,y:14,w:170,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[]), boxes:[ {t:'box-c',tx:'Using integers (0,1,2,3) for position lets us compare: "animPos > 1 means it\'s past the hallway". String comparisons like "hallway" === "hallway" work, but you can\'t do "hallway > far", numbers enable range checks.'}, {t:'box-t',tx:'Four positions map to four feelings: 0=safe, 1=worried, 2=scared, 3=too late. Each triggers different UI feedback (HUD dot colour, message box, music intensity). Discrete states create discrete emotional moments.'}, {t:'box-n',tx:'This is the entire animatronic AI reduced to its essence. Not pathfinding. Not physics. Just: a number that increments under certain conditions. Simple systems can produce complex-feeling gameplay.'}, ], quiz:{q:"Why use integers (0,1,2,3) instead of strings for position?",opts:["Strings don't work in JavaScript","Numbers allow range comparisons like 'pos > 1'","They're shorter to type"],correct:1}, onApply(){G.f_ai=true;draw();updateHUD();sout('✅ Animatronic position system active! Watch the tracker dots in the preview.');} }, {id:'s13
- Each tick, the AI runs a dice roll. If the roll beats the move chance, it advances one position. If the door is closed and the animatronic is at position 2, it can't enter. Wire this logic in the game loop.

## STEP 14 · AI GAMEOVER - On Game Tick',x:4,y:36,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'Door closed AND animPos = 2?',x:182,y:10,w:188,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Blocked?'}],rp:[{id:'r2t',type:'exec',label:'Blocked'},{id:'r2f',type:'exec',label:'Can Move'}]}, {id:'n3',type:'math',title:'Math.random() < chance',x:392,y:4,w:140,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3t',type:'exec',label:'True'},{id:'r3f',type:'exec',label:'False'}]}, {id:'n4',type:'func',title:'animPos++',sub:'Advance one step',x:392,y:60,w:140,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2f',t:'l3e'},{f:'r3t',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'Math.random() returns 0.0 to 1.0. Comparing it to 0.2 means there\'s a 20% chance it advances. This dice-roll approach makes the AI feel alive because it\'s probabilistic, you can\'t perfectly predict it.'}, {t:'box-n',tx:'The door block check runs FIRST (before the dice roll) because it\'s cheaper, a boolean check vs a random number calculation. Always check the cheaper condition first in branching logic. This is called "early exit" or "short-circuit evaluation".'}, {t:'box-s',tx:'At position 2 + door closed = blocked. At position 2 + door open = it can reach position 3 (inside) next tick. The door becomes critically important when animPos hits 2. This is why the tracker dots create urgency.'}, ], quiz:{q:"Why check the door BEFORE rolling the dice?",opts:["The door is more important","Boolean checks are cheaper, early exit saves processing","It doesn't matter"],correct:1}, onApply(){draw();sout('✅ Move logic wired! '+NIGHT.animName+' is now advancing through positions. Watch the dots!');} }, {id:'s14
- When animPos reaches 3, it's inside the office. The game is over instantly. Wire animPos === 3 ? clearInterval ? showAnimatronicGameOver. This path is different from the power-depletion game over.

## STEP 15 · TIMER - On animPos Update',x:4,y:34,w:165,rp:[{id:'r1',type:'exec',label:''},{id:'r1v',type:'int',label:'New Position'}]}, {id:'n2',type:'branch',title:'Branch',sub:'animPos === 3?',x:192,y:10,w:152,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Inside?'}],rp:[{id:'r2t',type:'exec',label:'GAME OVER'},{id:'r2f',type:'exec',label:'Continue'}]}, {id:'n3',type:'func',title:'clearInterval',x:366,y:4,w:150,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Animatronic Game Over',sub:'Show scare screen',x:366,y:58,w:160,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'Two game-over paths: (1) Power dies ? gradual, you see it coming. (2) Animatronic enters ? sudden, terror. The emotional difference matters. Good games have different failure modes that feel different.'}, {t:'box-n',tx:'Both game-over functions must call clearInterval. They\'re both valid endings. You can\'t share one clearInterval call, each branch needs its own. Forgetting this in either path = the runaway loop bug.'}, ], quiz:{q:"How many different game-over paths does this game have?",opts:["One","Two, power death and animatronic entry","Three"],correct:1}, onApply(){sout('✅ Animatronic game over path wired! Position 3 = instant game over. Try letting it reach the door without closing it!');} }, /* ----------- MODULE 5, NIGHT TIMER ----------- */ {id:'s15
- Game time is separate from real time. Each tick, advance gameTimeMins by 10. 360 game-minutes = 6 hours = Night 1 complete. Wire On Tick ? Add to gameTimeMins ? Update clock display.

## STEP 16 · WIN STATE - On Game Tick',x:4,y:34,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'math',title:'Add 10 Minutes',sub:'gameTimeMins + 10',x:182,y:10,w:152,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'int',label:'Current'}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2v',type:'int',label:'New Mins'}]}, {id:'n3',type:'vset',title:'Set gameTimeMins',x:358,y:4,w:162,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'int',label:'Value'}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Update Clock Display',sub:'Format HH:MM AM/PM',x:358,y:66,w:162,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2e',t:'l4e'},{f:'r2v',t:'l3v'}],[]), boxes:[ {t:'box-c',tx:'10 game-minutes per tick at 2000ms real-time means: 36 ticks × 10 mins = 360 mins = 1 real minute of play to complete a night (at default settings). The student controls this pacing via tick speed.'}, {t:'box-t',tx:'Format the display as HH:MM AM/PM. 0 mins = 12:00 AM. 60 mins = 1:00 AM. 360 mins = 6:00 AM = win. The "6 AM" target gives players a clear, satisfying countdown goal to focus on.'}, ], quiz:{q:"How many game-minutes equal one full night (6 hours)?",opts:["60","360","600"],correct:1}, onApply(){G.f_night=true;draw();sout('✅ Night timer running! Watch the clock in the top right of the preview advance toward 6 AM.');} }, {id:'s16
- Surviving until 6 AM is the win condition. Wire On Tick ? Branch (gameTimeMins = NIGHT_LENGTH) ? True ? clearInterval ? showVictory. Type the night length in minutes.

## STEP 17 · WARNINGS - On Game Tick',x:4,y:36,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'gameTimeMins = NIGHT_LENGTH?',x:182,y:10,w:195,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Morning?'}],rp:[{id:'r2t',type:'exec',label:'WIN!'},{id:'r2f',type:'exec',label:'Continue'}],vf:{hint:'Night length in mins (360)',val:''}}, {id:'n3',type:'func',title:'clearInterval',x:400,y:4,w:132,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Show 6 AM Victory',x:400,y:58,w:132,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'}],[{id:'n2',ok:v=>parseInt(v)>=60&&parseInt(v)<=720}]), boxes:[ {t:'box-c',tx:'360 minutes = 6 in-game hours = one full night. 180 minutes = faster 3-hour night for testing. 600 = a brutally long shift. The student controls night length, let them experiment with what feels right.'}, {t:'box-t',tx:'Two separate game endings now exist: power death (slow, inevitable) and 6 AM (triumph). The emotional contrast is what makes the win meaningful. Survial is relief BECAUSE of how bad failure feels.'}, {t:'box-n',tx:'The win condition check runs in the same tick function as the lose condition. Put the win check AFTER the drain and AI logic, so on the final tick, if both could trigger, the current-tick\'s actions resolve first.'}, ], quiz:{q:"Why put the win check AFTER drain and AI logic in the tick?",opts:["So current-tick actions resolve before ending","It doesn't matter","To make it harder"],correct:0}, onApply(v){NIGHT.nightLengthMins=parseInt(v['n2'])||360;updateInfoCard();initNight();sout('✅ Night length set to '+NIGHT.nightLengthMins+' mins. Survive to 6 AM!');} }, /* ----------- MODULE 6, POLISH ----------- */ {id:'s17
- Low power should feel visually dangerous. Wire the display update to check power thresholds and change the bar colour: teal (safe) ? orange (warning) ? red (critical). The player sees the danger building.

## STEP 18 · MESSAGES - updateDisplay',sub:'Called each tick',x:4,y:36,w:158,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'power > 60?',x:186,y:10,w:140,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Cond'}],rp:[{id:'r2t',type:'exec',label:'Safe'},{id:'r2f',type:'exec',label:'Check warn'}]}, {id:'n3',type:'func',title:'Set Teal (safe)',x:350,y:4,w:132,lp:[{id:'l3e',type:'exec',label:''}]}, {id:'n4',type:'branch',title:'Branch',sub:'power > warn?',x:350,y:58,w:140,lp:[{id:'l4e',type:'exec',label:''},{id:'l4c',type:'bool',label:'Cond'}],rp:[{id:'r4t',type:'exec',label:'Orange'},{id:'r4f',type:'exec',label:'Red'}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'Colour is communication. Teal = breathe. Orange = start worrying. Red = act NOW. This three-stage system maps to the player\'s emotional state without any words. It\'s called "affordance design".'}, {t:'box-t',tx:'The warning threshold (default 30%) triggers both colour change AND the animatronic aggression bonus from Step 11. Two independent systems both respond to the same state variable. That\'s emergent complexity from simple rules.'}, ], quiz:{q:"What colour indicates critical power level?",opts:["Teal","Orange","Red"],correct:2}, onApply(){draw();sout('✅ Warning colours active! Drop below 30% power to see the red critical state.');} }, {id:'s18
- Text messages tell the player what's happening: "Bonnie is at the door!". Wire a showMessage function that takes a string, displays it in the message box, and sets a timer to auto-clear it after a few ticks.

## STEP 19 · CAM ROOMS - showMessage(text)',sub:'Called by other events',x:4,y:34,w:175,rp:[{id:'r1',type:'exec',label:''},{id:'r1v',type:'string',label:'Message text'}]}, {id:'n2',type:'vset',title:'Set messageText',x:202,y:10,w:160,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'string',label:'Text'}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'vset',title:'Set msgTimer = 60',sub:'60 ticks display time',x:385,y:4,w:148,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'int',label:'60'}]}, ],[{f:'r1',t:'l2e'},{f:'r1v',t:'l2v'},{f:'r2',t:'l3e'}],[]), boxes:[ {t:'box-t',tx:'The timer auto-clears the message after 60 ticks. This prevents the message area filling up with stale text. In the game loop: if(msgTimer > 0) { msgTimer--; } else { clearMessage(); }'}, {t:'box-s',tx:'What triggers messages: light toggle, door click, camera toggle, animatronic position changes, low power warning, win, and both game-over types. Every significant state change should communicate to the player.'}, ], quiz:{q:"Why auto-clear messages after 60 ticks?",opts:["To save memory","To prevent stale text filling the message area","Messages can only show once"],correct:1}, onApply(){sout('✅ Message system wired! You\'ll now see event notifications in the office message bar.');} }, {id:'s19
- The camera shows what room the animatronic is in, but only if the camera is active. Wire Get animPos ? Map position to room name ? Display in camera feed. Camera off = no information.

## STEP 20 · NIGHTS - On Camera Toggle',x:4,y:36,w:162,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'vget',title:'Get animPos',x:188,y:16,w:138,rp:[{id:'r2',type:'int',label:'Position'}]}, {id:'n3',type:'func',title:'Map Pos ? Room',sub:'0=Office, 1=Hall, 2=Entrance',x:350,y:4,w:178,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'int',label:'Pos'}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3s',type:'string',label:'Room name'}]}, {id:'n4',type:'func',title:'Display on Monitor',x:350,y:66,w:162,lp:[{id:'l4e',type:'exec',label:''},{id:'l4s',type:'string',label:'Text'}]}, ],[{f:'r1',t:'l3e'},{f:'r2',t:'l3v'},{f:'r3e',t:'l4e'},{f:'r3s',t:'l4s'}],[]), boxes:[ {t:'box-c',tx:'The camera feed converts integer positions into human-readable room names. It\'s a display mapping, not actual game state. The real data is still animPos (0/1/2/3), the room name is just for the player\'s benefit.'}, {t:'box-t',tx:'This is the information asymmetry core to FNaF: the animatronic\'s position is a number you can\'t directly see. The camera is the only window into that data. Information costs power. FNaF is fundamentally a game about resource management.'}, ], quiz:{q:"What is the camera feed actually showing?",opts:["A live video","A display mapping of the animPos integer to a room name","The animatronic's health"],correct:1}, onApply(){draw();sout('✅ Camera room data mapped! The left monitor now shows which room the animatronic is in.');} }, {id:'s20
- After surviving Night 1, Night 2 begins with harder settings. Wire the win condition ? increment nightNumber ? increase animMoveChance ? restart the game state.

## STEP 21 · ANIM - On Night Complete',x:4,y:36,w:162,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'math',title:'Increment Night',sub:'nightNumber + 1',x:188,y:12,w:150,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'int',label:'Night'}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2v',type:'int',label:'Next Night'}]}, {id:'n3',type:'math',title:'Increase AI Chance',sub:'× 1.3',x:360,y:4,w:158,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'float',label:'Chance'}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3v',type:'float',label:'Harder'}]}, {id:'n4',type:'func',title:'Reset & Begin Night',x:360,y:66,w:158,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2e',t:'l4e'},{f:'r3v',t:'l3v'}],[]), boxes:[ {t:'box-c',tx:'The 1.3× multiplier per night means: Night 1 = 0.2 chance. Night 2 = 0.26. Night 3 = 0.34. Night 5 = 0.57. This is exponential difficulty scaling, the same mechanic feels completely different by night 5.'}, {t:'box-s',tx:'Reset these values between nights: playerPower ? 100, gameTimeMins ? 0, animPos ? 0, doorClosed ? false, lightOn ? false. A clean reset is essential, stale state from the previous night causes unpredictable bugs.'}, ], quiz:{q:"What's the difficulty multiplier per night?",opts:["1.1x","1.3x","2.0x"],correct:1}, onApply(){document.getElementById('hudNight').textContent='N2→';sout('✅ Night progression wired! Survive Night 1 and Night 2 auto-starts, harder.');} }, /* ----------- MODULE 7, ADVANCED ----------- */ {id:'s21
- Smooth transitions on the power bar make it feel alive. Wire a CSS transition so width and colour changes animate over 300ms rather than jumping. This is the difference between a prototype and a polished game.

## STEP 22 · CONTROLS - On BeginPlay',x:6,y:36,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Set CSS Transition',sub:'width + color ? 300ms ease',x:177,y:12,w:185,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'func',title:'Apply to Power Bar',sub:'style.transition = ...',x:385,y:12,w:148,lp:[{id:'l3e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'}],[]), boxes:[ {t:'box-c',tx:'CSS transitions intercept changes to a property and animate them. Without this, the bar snaps to each new width every 2 seconds. With it, you get a smooth fill that players can intuitively read as "draining".'}, {t:'box-t',tx:'Game feel (or "juice") is the term for these small feedback details that make games feel good. Smooth bars, screen shake on hits, sound effects on button presses, none of them change gameplay, but all of them change how players FEEL.'}, ], quiz:{q:"What is 'game juice'?",opts:["A power-up","Small feedback details that make games feel good","A type of variable"],correct:1}, onApply(){sout('✅ Power bar animation set to 300ms ease. The drain now feels smooth, not jerky.');} }, {id:'s22
- All buttons must disable during game over. Wire a single enableControls(true/false) function that toggles the disabled state on every interactive button. Call it from both game-over paths and on game start.

## STEP 23 · UTILITIES - enableControls(bool)',sub:'Central control toggle',x:4,y:14,w:188,lp:[{id:'l1e',type:'exec',label:''},{id:'l1b',type:'bool',label:'enabled'}],rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Set lightBtn.disabled',sub:'= !enabled',x:215,y:4,w:170,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'func',title:'Set camBtn.disabled',sub:'= !enabled',x:215,y:58,w:165,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Set doorBtn.disabled',sub:'= !enabled',x:215,y:112,w:165,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'A single function that controls all buttons is called "centralised state management". Instead of hunting through 6 places to disable a button, you call one function. If you add a 4th button later, you only update this one function.'}, {t:'box-u',tx:'disabled=true in HTML prevents click events firing entirely. This is better than checking "if(!gameRunning)" inside each button handler, it stops the event before it reaches your logic layer.'}, ], quiz:{q:"Why use one function to enable/disable ALL buttons?",opts:["It's less code","Centralised control, add a new button, update one function","JavaScript requires it"],correct:1}, onApply(){sout('✅ enableControls() wired! All buttons now enable/disable together based on game state.');} }, {id:'s23
- Raw minutes (e.g. 140) need to display as "2:20 AM". Wire a formatTime utility function: takes integer minutes ? returns formatted HH:MM AM/PM string. Then call it from the clock display.

## STEP 24 · BALANCE - formatTime(mins)',sub:'Utility Function',x:4,y:14,w:165,lp:[{id:'l1e',type:'exec',label:''},{id:'l1v',type:'int',label:'minutes'}],rp:[{id:'r1e',type:'exec',label:''},{id:'r1s',type:'string',label:'HH:MM AM/PM'}]}, {id:'n2',type:'math',title:'Hours = floor(mins/60)',x:192,y:4,w:168,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'int',label:'mins'}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2v',type:'int',label:'hrs'}]}, {id:'n3',type:'func',title:'Format ? String',sub:'"2:00 AM" etc.',x:382,y:4,w:152,lp:[{id:'l3e',type:'exec',label:''},{id:'l3h',type:'int',label:'hours'}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3s',type:'string',label:'Formatted'}]}, ],[{f:'r1e',t:'l2e'},{f:'r1v',t:'l2v'},{f:'r2e',t:'l3e'},{f:'r2v',t:'l3h'},{f:'r3e',t:'r1e'},{f:'r3s',t:'r1s'}],[]), boxes:[ {t:'box-c',tx:'A utility function does one job and does it well. formatTime converts data, it doesn\'t touch game state. This separation (data conversion vs game logic) makes code easier to test: you can call formatTime(125) and check it returns "2:05 AM" independently.'}, {t:'box-t',tx:'12-hour clock logic: 0 hours = 12 AM. 1-11 = 1-11 AM. 12 = 12 PM. 13+ = subtract 12 for PM hours. Use modulo: hrs % 12 or (hrs === 0 || hrs === 12 ? 12 : hrs % 12) for the display value.'}, ], quiz:{q:"What does a utility function do?",opts:["Changes game state","One job, converts data without touching game state","Draws graphics"],correct:1}, onApply(){sout('✅ formatTime utility wired! The clock now shows proper HH:MM AM/PM formatting.');} }, {id:'s24
- The door costs extra power every tick it's sealed. Wire the tick function to check doorClosed, if true, subtract doorPassiveDrain on top of the base passive drain. Then balance the three drain values together.

## STEP 25 · FULL GAME - On Game Tick',x:4,y:36,w:152,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Apply Base Drain',sub:'playerPower -= passiveDrain',x:178,y:12,w:172,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'branch',title:'Branch',sub:'doorClosed?',x:373,y:4,w:140,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'bool',label:'doorClosed'}],rp:[{id:'r3t',type:'exec',label:'Extra drain'},{id:'r3f',type:'exec',label:'Skip'}]}, {id:'n4',type:'func',title:'Apply Door Drain',sub:'-= doorPassiveDrain',x:373,y:64,w:158,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3e'},{f:'r3t',t:'l4e'}],[]), boxes:[ {t:'box-c',tx:'Total drain per tick = passiveDrain + (doorClosed ? doorDrain : 0) + (lightOn ? lightDrain : 0). Three independent boolean flags each contribute to a single cumulative drain. Understanding additive systems like this is key to game balance.'}, {t:'box-n',tx:'Game balancing test: start with passiveDrain=0.4, doorDrain=0.3, lightCost=15. A player who closes the door AND keeps the light on will drain 0.7+/tick extra. That\'s game-over in about 70 ticks of negligence, intentionally punishing.'}, {t:'box-s',tx:'Real FNaF 1 shows usage % per action as visual feedback. Our equivalent is the numbers in the action buttons. Transparency about costs helps players make informed decisions, opacity about costs is only fun when the reveal is part of the design.'}, ], quiz:{q:"How many drain sources compete in the game loop?",opts:["One","Two","Three, passive, door, and light"],correct:2}, onApply(){draw();sout('✅ Door drain integrated! Active door drains +'+NIGHT.doorPassiveDrain+'/tick. Manage three resource drains at once now.');} }, {id:'s25
- All systems are live. Power drains, time advances, the animatronic moves, the door blocks, cameras cost power. Can you survive until 6 AM? Click buttons in the office preview to play.
