Download index.html from IT-BOS/VGgame: direct link, hf CLI and curl.
- Browser
- Download file 89.3 kB
-
https://huggingface.co/spaces/IT-BOS/VGgame/resolve/main/index.html
- Command line
-
hf download hf://spaces/IT-BOS/VGgame/index.html
-
curl -L -o index.html https://huggingface.co/spaces/IT-BOS/VGgame/resolve/main/index.html
89.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>VirtuGuard Command Center: Agent Training</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{ | |
| --navy:#272f4f; --navy-deep:#161b33; --navy-ink:#0d1024; | |
| --teal:#00a9a5; --teal-dim:#0f6f6d; --amber:#f2b632; --red:#e0483e; | |
| --paper:#e6ebf1; --muted:#9aa6bd; --line:#3b4468; | |
| --pix:'Press Start 2P', monospace; --body:Arial, Helvetica, sans-serif; | |
| } | |
| *{box-sizing:border-box} | |
| html,body{margin:0;background:var(--navy-ink);color:var(--paper);font-family:var(--body)} | |
| body{min-height:100vh;display:flex;justify-content:center;padding:16px 12px} | |
| .console{width:100%;max-width:820px} | |
| header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px} | |
| header .logo{height:34px;width:auto} header .logo path{fill:#e6ebf1 } | |
| .rlogo{height:40px;width:auto;margin:0 0 14px;display:block} .rlogo path{fill:#00a9a5 } | |
| .hud{font-family:var(--pix);font-size:10px;color:var(--teal);display:flex;gap:14px;align-items:center;flex-wrap:wrap;justify-content:flex-end} | |
| .hud .score{color:var(--amber)} | |
| .btn-icon{background:none;border:2px solid var(--line);color:var(--muted);font-family:var(--pix);font-size:9px;padding:6px 8px;cursor:pointer;border-radius:2px} | |
| .btn-icon:hover,.btn-icon:focus-visible{border-color:var(--teal);color:var(--paper);outline:none} | |
| .monitor{background:var(--navy);border:4px solid var(--line);border-radius:6px;padding:8px;box-shadow:inset 0 0 0 2px #1a2040} | |
| .screen{position:relative;background:#000;aspect-ratio:16/9;overflow:hidden;border-radius:2px} | |
| canvas{width:100%;height:100%;display:block;image-rendering:pixelated;image-rendering:crisp-edges} | |
| .scan{position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(0,0,0,.18) 0 1px,transparent 1px 3px);mix-blend-mode:multiply} | |
| .panel{background:var(--navy-deep);border:2px solid var(--line);border-top:none;padding:14px 16px 16px} | |
| .title{font-family:var(--pix);font-size:10px;color:var(--teal);margin:0 0 10px;line-height:1.7;display:flex;gap:10px;align-items:center;flex-wrap:wrap} | |
| .tag{font-size:8px;padding:3px 6px;border:1px solid var(--teal);color:var(--teal);border-radius:2px} | |
| .tag.patrol{border-color:var(--muted);color:var(--muted)} | |
| .tag.alarm{border-color:var(--red);color:var(--red)} | |
| .dots{display:flex;gap:4px;margin-left:auto} .dots i{width:8px;height:8px;background:var(--line);display:block;border-radius:1px} .dots i.on{background:var(--teal)} .dots i.now{background:var(--amber)} | |
| .narr{font-size:16px;line-height:1.55;margin:0 0 14px;max-width:70ch} | |
| .narr b{color:var(--amber);font-weight:700} | |
| .actions{display:grid;grid-template-columns:repeat(4,1fr);gap:8px} | |
| .act{position:relative;background:var(--navy);border:2px solid var(--line);border-bottom-width:5px;color:var(--paper);font-family:var(--pix);font-size:9px;line-height:1.5;padding:12px 6px;cursor:pointer;border-radius:3px;text-align:center;transition:transform .05s} | |
| .act:hover,.act:focus-visible{border-color:var(--teal);outline:none} | |
| .act:active{transform:translateY(3px);border-bottom-width:2px} | |
| .act[disabled]{opacity:.4;cursor:default;transform:none} | |
| .act .k{position:absolute;top:3px;left:5px;font-size:7px;color:var(--muted)} | |
| .act.v{border-top:3px solid var(--teal)} .act.s{border-top:3px solid var(--amber)} .act.d{border-top:3px solid var(--red)} .act.c{border-top:3px solid var(--muted)} | |
| .feedback{margin-top:14px;padding:12px 14px;border-left:4px solid var(--teal);background:#1c2340;font-size:15px;line-height:1.5} | |
| .feedback.bad{border-left-color:var(--red)} .feedback.ok{border-left-color:var(--amber)} | |
| .feedback .pts{font-family:var(--pix);font-size:10px;display:block;margin-bottom:8px;color:var(--teal)} | |
| .feedback.bad .pts{color:var(--red)} .feedback.ok .pts{color:var(--amber)} | |
| .feedback .row{margin-top:12px} .feedback .next{margin-top:0} | |
| .next{margin-top:12px;background:var(--teal);color:#062322;border:none;border-bottom:4px solid var(--teal-dim);font-family:var(--pix);font-size:10px;padding:12px 16px;cursor:pointer;border-radius:3px} | |
| .next:hover,.next:focus-visible{background:#19c4bf;outline:none} | |
| .next.ghost{background:transparent;color:var(--paper);border:2px solid var(--line);border-bottom-width:4px} | |
| .next.ghost:hover{border-color:var(--teal)} | |
| .next.amber{background:var(--amber);border-bottom-color:#a67a14;color:#2b2005} .next.amber:hover{background:#ffc94a} | |
| .restart{margin-top:14px} | |
| .hint{font-size:12px;color:var(--muted);margin:10px 0 0} | |
| .menu{padding:22px 16px} | |
| .menu h1{font-family:var(--pix);font-size:15px;color:var(--paper);line-height:1.7;margin:0 0 8px} | |
| .menu p{font-size:16px;line-height:1.55;color:var(--muted);margin:0 0 18px;max-width:64ch} | |
| .sites{display:grid;grid-template-columns:repeat(3,1fr);gap:10px} | |
| .site{background:var(--navy);border:2px solid var(--line);border-bottom-width:5px;padding:0 0 10px;cursor:pointer;color:var(--paper);text-align:left;border-radius:3px;overflow:hidden;position:relative} | |
| .site:hover,.site:focus-visible{border-color:var(--teal);outline:none} | |
| .site canvas{aspect-ratio:16/9;width:100%} | |
| .site span{display:block;font-family:var(--pix);font-size:9px;line-height:1.6;padding:10px 10px 0;color:var(--teal)} | |
| .site small{display:block;font-size:13px;color:var(--muted);padding:2px 10px 0} | |
| .site em{position:absolute;top:6px;right:6px;font-style:normal;font-family:var(--pix);font-size:7px;color:var(--amber);background:rgba(13,16,36,.8);padding:3px 5px;border-radius:2px} | |
| .result h2{font-family:var(--pix);font-size:13px;line-height:1.7;margin:0 0 6px;color:var(--amber)} | |
| .result .big{font-family:var(--pix);font-size:28px;color:var(--paper);margin:8px 0 12px} | |
| .result .big small{font-size:11px;color:var(--muted)} | |
| .recap{list-style:none;padding:0;margin:0 0 16px;font-size:14px;line-height:1.6;color:var(--muted)} | |
| .recap li{padding:4px 0;border-bottom:1px solid #222a48} .recap li b{color:var(--paper);font-weight:normal} | |
| .cta{margin:18px 0 0;padding:16px;border:2px solid var(--teal);border-radius:3px;background:#0f2a2c} | |
| .cta p{margin:0 0 12px;font-size:15px;line-height:1.5} | |
| .row{display:flex;gap:10px;flex-wrap:wrap} | |
| a.next{text-decoration:none;display:inline-block} | |
| .hidden{display:none} | |
| /* Phone layout: camera stays pinned while you read and act below it, sites become a swipe row, bigger tap targets */ | |
| @media (max-width:600px){ | |
| body{padding:8px 8px 24px} | |
| header{margin-bottom:6px} header .logo{height:24px} | |
| .hud{font-size:8px;gap:8px} .btn-icon{font-size:8px;padding:5px 6px} | |
| .monitor{position:sticky;top:0;z-index:5;padding:5px;border-width:3px;border-radius:5px;box-shadow:0 6px 18px rgba(0,0,0,.55)} | |
| .panel{padding:12px 12px 14px} | |
| .title{font-size:9px;gap:8px} .dots{width:100%;margin-left:0;order:9} | |
| .narr,.menu p{font-size:15px;line-height:1.5} | |
| .actions{grid-template-columns:repeat(2,1fr);gap:8px} | |
| .act{min-height:56px;font-size:8px;padding:12px 8px;display:flex;align-items:center;justify-content:center} .act .k{display:none} | |
| .feedback{font-size:14px;padding:12px} .feedback .row{flex-direction:column} .feedback .next{width:100%} | |
| .menu{padding:16px 12px 18px} .menu h1{font-size:13px} | |
| .sites{display:flex;overflow-x:auto;gap:10px;scroll-snap-type:x mandatory;padding:2px 2px 10px;margin:0 -2px;-webkit-overflow-scrolling:touch;scrollbar-width:none} | |
| .sites::-webkit-scrollbar{display:none} | |
| .site{flex:0 0 78%;scroll-snap-align:center} | |
| .hint{display:none} .swipe{display:block} | |
| .result .big{font-size:22px} .cta .row{flex-direction:column} .cta .next{width:100%;text-align:center} | |
| .restart{width:100%} | |
| } | |
| /* Touch devices of any size: no hover-only affordances, larger buttons */ | |
| @media (pointer:coarse){ .act{min-height:52px} .next{padding:14px 16px} } | |
| .swipe{display:none;font-size:12px;color:var(--muted);margin:8px 0 0} | |
| @media (prefers-reduced-motion:reduce){ .act,.act:active{transition:none;transform:none} } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="console"> | |
| <header> | |
| <svg class="logo" role="img" aria-label="VirtuGuard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 241.42 53.33"><g><path d="M88.68,18.07h-4.88l-6.99,17.99-6.96-17.99h-4.88l9.6,23.21h4.52l9.6-23.21h-.01Zm5.74,6.79v16.32h-4.02V24.86h4.02Zm-4.39-4.52c0,3.13,4.75,3.13,4.75,0s-4.75-3.13-4.75,0h0Zm8.01,4.58v16.26h4.02v-8.57c0-3,1.95-4.12,4.06-4.12,1.32,0,2.08,.36,2.9,1.06l1.81-3.5c-.89-.89-2.51-1.55-4.19-1.55s-3.33,.3-4.58,2.31l-.3-1.88h-3.72Zm16.98-4.16v4.19h-3.03v3.46h3.03v7.02c0,4.06,2.31,6.07,5.84,5.94,1.22-.03,2.18-.23,3.33-.69l-1.12-3.43c-.59,.3-1.32,.49-1.95,.49-1.25,0-2.11-.76-2.11-2.31v-7.02h4.52v-3.46h-4.48v-4.62l-4.02,.43h-.01Zm11.61,4.15v8.54c0,5.04,2.84,7.98,6.73,7.98,2.28,0,3.82-.79,5.51-2.44l.26,2.18h3.59V24.92h-3.99v8.29c0,2.49-1.68,4.57-4.15,4.57s-3.92-1.88-3.92-4.35v-8.51h-4.03Z" style="fill:#272f4f; fill-rule:evenodd;"></path><path d="M166.44,20.97c-2.37-2.34-5.47-3.53-8.57-3.53-8.14,0-12.2,6-12.2,12.5,0,6.07,3.53,11.77,12.2,11.77,4.06,0,6.92-1.55,9.2-4.15v-9.73h-9.89v3.89h5.9v4.09c-1.81,1.45-3.4,1.85-5.21,1.85-5.74,0-7.95-3.92-7.95-7.72,0-4.75,3-8.51,7.95-8.51,2.01,0,4.32,.82,5.87,2.31l2.7-2.77h0Zm3.99,3.96v8.54c0,5.04,2.84,7.98,6.73,7.98,2.28,0,3.82-.79,5.51-2.44l.26,2.18h3.59V24.93h-3.99v8.29c0,2.49-1.68,4.57-4.15,4.57s-3.92-1.88-3.92-4.35v-8.51h-4.03Zm32.41,2.24c-.92-1.62-3-2.64-5.18-2.64-4.75-.03-8.47,2.9-8.47,8.51s3.56,8.67,8.38,8.64c1.81-.03,4.35-.96,5.28-2.87l.2,2.37h3.79V24.92h-3.86l-.13,2.24h0Zm-4.81,.97c6.33,0,6.33,9.86,0,9.86-2.67,0-4.81-1.85-4.81-4.95s2.14-4.91,4.81-4.91h0Zm12.2-3.2v16.26h4.02v-8.57c0-3,1.95-4.12,4.06-4.12,1.32,0,2.08,.36,2.9,1.06l1.81-3.5c-.89-.89-2.51-1.55-4.19-1.55s-3.33,.3-4.58,2.31l-.3-1.88h-3.72Zm22.19,3.35c2.54,0,4.72,1.91,4.72,4.75s-2.18,4.78-4.72,4.78-4.62-1.95-4.62-4.78,2.04-4.75,4.62-4.75h0Zm4.98-10.16v9c-.96-1.68-3.63-2.61-5.34-2.61-4.75,0-8.28,2.9-8.28,8.51s3.59,8.51,8.38,8.51c1.98,0,3.99-.66,5.24-2.6l.26,2.24h3.76V18.13h-4.02Z" style="fill:#00a9a5; fill-rule:evenodd;"></path></g><g><path d="M9.26,6.15c1.52-.28,2.99-.63,4.4-1.05,3.38-1,6.44-2.4,9.16-4.19l1.38-.91,1.38,.91c2.72,1.79,5.78,3.19,9.16,4.19,1.41,.42,2.88,.77,4.4,1.05-.03,1.76-.09,3.45-.16,5.07h0c-.08,1.62-.17,3.18-.29,4.66l-2.21-.45c-.9-.18-1.81-.39-2.71-.62h0c.23-2.4,.34-3.95,.32-4.67-.25-.07-.51-.14-.76-.22-3.29-.98-6.34-2.3-9.12-3.96-2.79,1.66-5.83,2.98-9.12,3.96-.25,.08-.5,.15-.76,.22-.02,.71,.09,2.27,.32,4.67h0c.14,1.41,.31,3.1,.53,5.04h0c.47,3.3,1.31,5.94,2.67,8.28,1.43,2.44,3.48,4.65,6.36,7.04,2.88-2.39,4.93-4.6,6.36-7.04,.62-1.06,1.13-2.18,1.54-3.39-1.02-.27-2.03-.57-3.02-.89-1.67-.55-3.3-1.17-4.88-1.88-1.14,.51-2.3,.98-3.49,1.4-.36-1.33-.61-3.02-.76-5.08,1.06-.41,2.12-.87,3.17-1.37l1.08-.52,1.08,.52c1.76,.84,3.55,1.55,5.36,2.15,.86,.28,1.73,.54,2.59,.78,.18,.05,.35,.09,.53,.14l4.33,1.27c-.58,3.65-1.58,6.65-3.19,9.4-2.03,3.47-4.95,6.44-9.16,9.72l-1.54,1.2-1.54-1.2c-4.21-3.28-7.13-6.24-9.16-9.72-2.04-3.5-3.11-7.41-3.58-12.53l-.2-2.24c-.12-1.48-.21-3.03-.29-4.66-.08-1.62-.13-3.31-.16-5.07h-.02Z" style="fill:#00a9a5; fill-rule:evenodd;"></path><path d="M22.77,52.34c-8.92-6.18-14.6-11.28-18.11-17.73C1.15,28.14-.05,20.55,0,9.43v-2.38l2.39-.12c.87-.05,1.73-.11,2.57-.18-.02,.98,0,2.66,.07,5.04,.12,8.98,1.23,15.23,4.04,20.42,2.88,5.31,7.67,9.73,15.13,15.01,7.46-5.28,12.25-9.69,15.13-15.01,2.82-5.19,3.92-11.44,4.04-20.42h0c.06-2.38,.08-4.05,.07-5.04,.84,.08,1.7,.14,2.57,.18l2.38,.12v2.38c.06,11.12-1.14,18.71-4.65,25.18-3.5,6.46-9.18,11.56-18.11,17.73l-1.43,.99-1.43-.99Z" style="fill:#272f4f; fill-rule:evenodd;"></path><path d="M51.83,3.85h.72V.66h1.22V0h-3.16V.66h1.22V3.85h0Zm5.73,0h.72V.02h-.83l-1.2,1.65L55.05,.02h-.83V3.85h.72V1.06l1.22,1.67h.14l1.25-1.66V3.85h0Z" style="fill:#272f4f; fill-rule:evenodd;"></path></g></svg> | |
| <div class="hud"><span id="hudCam">CAM --</span><span class="score" id="hudScore"></span> | |
| <button class="btn-icon" id="mute" aria-pressed="false" title="Toggle sound">SOUND ON</button></div> | |
| </header> | |
| <div class="monitor"> | |
| <div class="screen"><canvas id="cv" width="320" height="180" aria-label="Camera view"></canvas><div class="scan"></div></div> | |
| </div> | |
| <div class="panel menu" id="menu"> | |
| <h1>Take the agent seat.</h1> | |
| <p>Pick a site. Alarms and scheduled patrols come in, you decide what happens next. Verify what you see, use the right tool at the right time, and know when to bring in officers. Points come from judgment, not speed.</p> | |
| <div class="sites" id="sites"></div> | |
| <p class="swipe">Swipe to see all seven sites.</p> | |
| <p class="hint">Sound is on. Use the button top right to mute. Keyboard: 1 to 7 pick an action, Enter continues.</p> | |
| </div> | |
| <div class="panel hidden" id="play"> | |
| <p class="title"><span id="stepNum"></span><span class="tag" id="stepTag"></span><span id="stepTitle"></span><span class="dots" id="dots"></span></p> | |
| <p class="narr" id="narr"></p> | |
| <div class="actions" id="actions"></div> | |
| <div id="fb" class="feedback hidden"><span class="pts" id="fbPts"></span><span id="fbText"></span> | |
| <span class="row"><button class="next" id="nextBtn">Continue</button><button class="next ghost" id="redoBtn">Choose again</button></span></div> | |
| <button class="btn-icon restart" id="restart">Restart site</button> | |
| </div> | |
| <div class="panel result hidden" id="result"> | |
| <svg class="rlogo" role="img" aria-label="VirtuGuard" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 241.42 53.33"><g><path d="M88.68,18.07h-4.88l-6.99,17.99-6.96-17.99h-4.88l9.6,23.21h4.52l9.6-23.21h-.01Zm5.74,6.79v16.32h-4.02V24.86h4.02Zm-4.39-4.52c0,3.13,4.75,3.13,4.75,0s-4.75-3.13-4.75,0h0Zm8.01,4.58v16.26h4.02v-8.57c0-3,1.95-4.12,4.06-4.12,1.32,0,2.08,.36,2.9,1.06l1.81-3.5c-.89-.89-2.51-1.55-4.19-1.55s-3.33,.3-4.58,2.31l-.3-1.88h-3.72Zm16.98-4.16v4.19h-3.03v3.46h3.03v7.02c0,4.06,2.31,6.07,5.84,5.94,1.22-.03,2.18-.23,3.33-.69l-1.12-3.43c-.59,.3-1.32,.49-1.95,.49-1.25,0-2.11-.76-2.11-2.31v-7.02h4.52v-3.46h-4.48v-4.62l-4.02,.43h-.01Zm11.61,4.15v8.54c0,5.04,2.84,7.98,6.73,7.98,2.28,0,3.82-.79,5.51-2.44l.26,2.18h3.59V24.92h-3.99v8.29c0,2.49-1.68,4.57-4.15,4.57s-3.92-1.88-3.92-4.35v-8.51h-4.03Z" style="fill:#272f4f; fill-rule:evenodd;"></path><path d="M166.44,20.97c-2.37-2.34-5.47-3.53-8.57-3.53-8.14,0-12.2,6-12.2,12.5,0,6.07,3.53,11.77,12.2,11.77,4.06,0,6.92-1.55,9.2-4.15v-9.73h-9.89v3.89h5.9v4.09c-1.81,1.45-3.4,1.85-5.21,1.85-5.74,0-7.95-3.92-7.95-7.72,0-4.75,3-8.51,7.95-8.51,2.01,0,4.32,.82,5.87,2.31l2.7-2.77h0Zm3.99,3.96v8.54c0,5.04,2.84,7.98,6.73,7.98,2.28,0,3.82-.79,5.51-2.44l.26,2.18h3.59V24.93h-3.99v8.29c0,2.49-1.68,4.57-4.15,4.57s-3.92-1.88-3.92-4.35v-8.51h-4.03Zm32.41,2.24c-.92-1.62-3-2.64-5.18-2.64-4.75-.03-8.47,2.9-8.47,8.51s3.56,8.67,8.38,8.64c1.81-.03,4.35-.96,5.28-2.87l.2,2.37h3.79V24.92h-3.86l-.13,2.24h0Zm-4.81,.97c6.33,0,6.33,9.86,0,9.86-2.67,0-4.81-1.85-4.81-4.95s2.14-4.91,4.81-4.91h0Zm12.2-3.2v16.26h4.02v-8.57c0-3,1.95-4.12,4.06-4.12,1.32,0,2.08,.36,2.9,1.06l1.81-3.5c-.89-.89-2.51-1.55-4.19-1.55s-3.33,.3-4.58,2.31l-.3-1.88h-3.72Zm22.19,3.35c2.54,0,4.72,1.91,4.72,4.75s-2.18,4.78-4.72,4.78-4.62-1.95-4.62-4.78,2.04-4.75,4.62-4.75h0Zm4.98-10.16v9c-.96-1.68-3.63-2.61-5.34-2.61-4.75,0-8.28,2.9-8.28,8.51s3.59,8.51,8.38,8.51c1.98,0,3.99-.66,5.24-2.6l.26,2.24h3.76V18.13h-4.02Z" style="fill:#00a9a5; fill-rule:evenodd;"></path></g><g><path d="M9.26,6.15c1.52-.28,2.99-.63,4.4-1.05,3.38-1,6.44-2.4,9.16-4.19l1.38-.91,1.38,.91c2.72,1.79,5.78,3.19,9.16,4.19,1.41,.42,2.88,.77,4.4,1.05-.03,1.76-.09,3.45-.16,5.07h0c-.08,1.62-.17,3.18-.29,4.66l-2.21-.45c-.9-.18-1.81-.39-2.71-.62h0c.23-2.4,.34-3.95,.32-4.67-.25-.07-.51-.14-.76-.22-3.29-.98-6.34-2.3-9.12-3.96-2.79,1.66-5.83,2.98-9.12,3.96-.25,.08-.5,.15-.76,.22-.02,.71,.09,2.27,.32,4.67h0c.14,1.41,.31,3.1,.53,5.04h0c.47,3.3,1.31,5.94,2.67,8.28,1.43,2.44,3.48,4.65,6.36,7.04,2.88-2.39,4.93-4.6,6.36-7.04,.62-1.06,1.13-2.18,1.54-3.39-1.02-.27-2.03-.57-3.02-.89-1.67-.55-3.3-1.17-4.88-1.88-1.14,.51-2.3,.98-3.49,1.4-.36-1.33-.61-3.02-.76-5.08,1.06-.41,2.12-.87,3.17-1.37l1.08-.52,1.08,.52c1.76,.84,3.55,1.55,5.36,2.15,.86,.28,1.73,.54,2.59,.78,.18,.05,.35,.09,.53,.14l4.33,1.27c-.58,3.65-1.58,6.65-3.19,9.4-2.03,3.47-4.95,6.44-9.16,9.72l-1.54,1.2-1.54-1.2c-4.21-3.28-7.13-6.24-9.16-9.72-2.04-3.5-3.11-7.41-3.58-12.53l-.2-2.24c-.12-1.48-.21-3.03-.29-4.66-.08-1.62-.13-3.31-.16-5.07h-.02Z" style="fill:#00a9a5; fill-rule:evenodd;"></path><path d="M22.77,52.34c-8.92-6.18-14.6-11.28-18.11-17.73C1.15,28.14-.05,20.55,0,9.43v-2.38l2.39-.12c.87-.05,1.73-.11,2.57-.18-.02,.98,0,2.66,.07,5.04,.12,8.98,1.23,15.23,4.04,20.42,2.88,5.31,7.67,9.73,15.13,15.01,7.46-5.28,12.25-9.69,15.13-15.01,2.82-5.19,3.92-11.44,4.04-20.42h0c.06-2.38,.08-4.05,.07-5.04,.84,.08,1.7,.14,2.57,.18l2.38,.12v2.38c.06,11.12-1.14,18.71-4.65,25.18-3.5,6.46-9.18,11.56-18.11,17.73l-1.43,.99-1.43-.99Z" style="fill:#272f4f; fill-rule:evenodd;"></path><path d="M51.83,3.85h.72V.66h1.22V0h-3.16V.66h1.22V3.85h0Zm5.73,0h.72V.02h-.83l-1.2,1.65L55.05,.02h-.83V3.85h.72V1.06l1.22,1.67h.14l1.25-1.66V3.85h0Z" style="fill:#272f4f; fill-rule:evenodd;"></path></g></svg> | |
| <h2 id="rTitle"></h2> | |
| <div class="big"><span id="rScore"></span> <small id="rMax"></small></div> | |
| <p class="narr" id="rNote"></p> | |
| <ul class="recap" id="recap"></ul> | |
| <div class="cta"> | |
| <p>That is one shift at one site. VirtuGuard agents do this around the clock, backed by AI detection that filters the noise so a trained person is watching when it matters.</p> | |
| <div class="row"> | |
| <a class="next amber" href="https://www.bossecurity.com/virtuguard/" target="_blank" rel="noopener">Visit VirtuGuard</a> | |
| <a class="next ghost" href="https://www.bossecurity.com/virtuguard/learn-more-about-virtuguard/" target="_blank" rel="noopener">Talk to us about your site</a> | |
| </div> | |
| </div> | |
| <div class="row" style="margin-top:14px"> | |
| <button class="next ghost" id="again">Play another site</button> | |
| <button class="next ghost" id="replay">Replay this site</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| /* ============ Actions ============ */ | |
| const ACTIONS = [ | |
| {id:'watch', cls:'v', label:'Watch and report'}, | |
| {id:'voice', cls:'v', label:'Voice down'}, | |
| {id:'strobe', cls:'s', label:'Strobes'}, | |
| {id:'siren', cls:'s', label:'Siren'}, | |
| {id:'guard', cls:'d', label:'Dispatch security'}, | |
| {id:'police', cls:'d', label:'Dispatch police'}, | |
| {id:'clear', cls:'c', label:'Clear event'}, | |
| ]; | |
| const D = { | |
| watch: {pts:0, txt:'You take another look. Nothing new on screen. The situation still needs a decision.'}, | |
| voice: {pts:-5, txt:'A voice down here does not fit what is on camera. Save it for a person who needs to hear it.'}, | |
| strobe: {pts:-5, txt:'Strobes fire across an empty scene. Deterrents work best once a subject has been warned.'}, | |
| siren: {pts:-5, txt:'Siren blares with no one to move. That is a noise complaint waiting to happen.'}, | |
| guard: {pts:-5, txt:'There is nothing yet for a guard to respond to.'}, | |
| police: {pts:-15,txt:'Police get called on confirmed activity. Sending them early wastes their trust and can cost the client a false alarm fee.'}, | |
| clear: {pts:-15,txt:'Clearing an open event without confirming what happened is how incidents get missed.'}, | |
| }; | |
| const NOGUARD={pts:-5,txt:'This site is monitoring only after hours. No guard to dispatch.',go:'stay'}; | |
| /* ============ Scenarios ============ */ | |
| const SCENARIOS = [ | |
| { id:'dealer', name:'Auto dealership', sub:'Night lot, 1:47 AM', cam:'CAM 01 FRONT ROW', steps:[ | |
| { kind:'alarm', title:'Motion, front row', cam:'CAM 01 FRONT ROW', | |
| narr:'A flatbed pulls onto the front row with a sedan on the back. The lot closed at 9 PM.', | |
| scene:{actors:[{t:'tow',x:200,y:118}]}, | |
| opts:{ watch:{pts:10,txt:'Live view: the truck carries the logo of the client\'s transport company, and the site notes list them as approved for after-hours deliveries.',go:'next'}, | |
| voice:{pts:-5,txt:'You almost shout at the client\'s own delivery driver. Check the site notes before you key the speaker.',go:'stay'}, | |
| clear:{pts:-10,txt:'You have not looked yet. A truck on a closed lot could be a rollback thief just as easily as a delivery.',go:'stay'} }}, | |
| { kind:'alarm', title:'Approved delivery', cam:'CAM 01 FRONT ROW', | |
| narr:'The driver unloads the sedan and heads back to the truck. Everything matches the approved list.', | |
| scene:{actors:[{t:'tow',x:200,y:118},{t:'person',x:190,y:120,c:'#c9a66b'}]}, | |
| opts:{ clear:{pts:10,txt:'Cleared with a note: approved transport delivery, sedan dropped front row. A clean audit trail matters as much as a catch.',go:'next'}, | |
| watch:{pts:5,txt:'Reasonable to watch him leave. Once he is off the lot, close the event with a note.',go:'next'} }}, | |
| { kind:'alarm', title:'Person detected, north lot', cam:'CAM 03 NORTH LOT', | |
| narr:'Twenty minutes later. A figure walks between the rows of trucks on the north lot. AI flags <b>person detected</b>.', | |
| scene:{actors:[{t:'person',x:150,y:112,c:'#5c6470',pose:'walk'}]}, | |
| opts:{ watch:{pts:10,txt:'Live view confirms an adult in a gray hoodie moving truck to truck, pulling on door handles. No vehicle, no badge, no reason to be there.',go:'next'}, | |
| voice:{pts:5,txt:'Fair instinct, and it works, but protocol is verify first so you never voice down the general manager grabbing a file.',go:'next'}, | |
| clear:{pts:-15,txt:'AI flagged a person and the lot is closed. You do not clear that without looking.',go:'stay'} }}, | |
| { kind:'alarm', title:'Subject checking door handles', cam:'CAM 03 NORTH LOT', | |
| narr:'He stops at a white pickup and tries the driver door. Locked. He cups his hands against the glass.', | |
| scene:{actors:[{t:'person',x:118,y:112,c:'#5c6470',pose:'reach'}]}, | |
| opts:{ voice:{pts:15,txt:'Live audio: "You in the gray hoodie at the white pickup, you are on camera and this property is being monitored. Leave now." He freezes and looks for the speaker.',go:'next',fx:'voice'}, | |
| strobe:{pts:5,txt:'The strobes get his attention, but a live voice naming his clothing is what proves a human is watching. Use it first.',go:'next',fx:'strobe'}, | |
| siren:{pts:5,txt:'Noise makes him flinch. A voice down naming what he is wearing lands harder. Use it first.',go:'next',fx:'siren'}, | |
| police:{pts:0,txt:'Hold that. Most subjects leave within seconds of a voice down. Police are for the ones who stay.',go:'stay'}, | |
| guard:NOGUARD }}, | |
| { kind:'alarm', title:'Subject not complying', cam:'CAM 03 NORTH LOT', | |
| narr:'He ignores the warning, pulls a tool from his pocket and starts working the window seal.', | |
| scene:{actors:[{t:'person',x:118,y:112,c:'#5c6470',pose:'reach'}],spark:true}, | |
| opts:{ strobe:{pts:15,txt:'Strobes light the lot. He drops the tool and backs away from the truck.',go:'next',fx:'strobe'}, | |
| siren:{pts:15,txt:'The siren hits and he drops the tool and backs off. Now he knows the whole street can hear it.',go:'next',fx:'siren'}, | |
| voice:{pts:5,txt:'Second warning delivered. He hesitates, then goes back to the window. Time to escalate.',go:'stay',fx:'voice'}, | |
| police:{pts:10,txt:'Defensible call. Officers are on the way, but firing the on-site deterrents first often ends it before they roll.',go:'next'}, | |
| guard:NOGUARD }}, | |
| { kind:'alarm', title:'Subject still on property', cam:'CAM 04 EAST FENCE', | |
| narr:'He backs off the truck but does not leave. He crouches behind a silver sedan near the east fence, watching the road.', | |
| scene:{actors:[{t:'person',x:236,y:118,c:'#5c6470',pose:'crouch'}]}, | |
| opts:{ police:{pts:15,txt:'Dispatch is on the line. You give it clean: male, gray hoodie, jeans, tool in hand, crouched behind a silver sedan at the east fence. You stay on the call.',go:'next',fx:'police'}, | |
| voice:{pts:5,txt:'Third warning. He is not leaving. Bring in officers and keep your eyes on him.',go:'stay',fx:'voice'}, | |
| siren:{pts:0,txt:'Siren again. He stays put. Deterrents have done their job; this one needs officers.',go:'stay',fx:'siren'}, | |
| clear:{pts:-15,txt:'He is still on the lot with a tool. This is the exact moment the client is paying you for.',go:'stay'} }}, | |
| { kind:'alarm', title:'Officers arriving', cam:'CAM 05 SERVICE BAYS', | |
| narr:'Two units pull in the front entrance. The subject moves toward the rear service bays. Dispatch asks where he is now.', | |
| scene:{actors:[{t:'person',x:70,y:114,c:'#5c6470',pose:'walk'},{t:'police',x:236,y:104}]}, | |
| opts:{ watch:{pts:15,txt:'"Behind the service bays, north side, moving west along the wall." Officers come around both ends and take him without a chase. Event documented, client notified.',go:'end'}, | |
| police:{pts:5,txt:'They are already here. What dispatch needs now is your eyes. You relay his position and officers take him at the service bays.',go:'end'}, | |
| siren:{pts:-5,txt:'Not now. Officers on scene need to hear each other. Relay his location instead.',go:'stay'} }}, | |
| ]}, | |
| { id:'multi', name:'Multifamily housing', sub:'Garden apartments, 11:52 PM', cam:'CAM 02 POOL DECK', steps:[ | |
| { kind:'alarm', title:'Loitering, pool deck', cam:'CAM 02 POOL DECK', | |
| narr:'Two people on the pool deck. Pool hours ended at 10 PM.', | |
| scene:{actors:[{t:'person',x:196,y:112,c:'#d8895b'},{t:'person',x:212,y:112,c:'#7fb3d5'}]}, | |
| opts:{ watch:{pts:10,txt:'Two young adults on lounge chairs, one holding a key fob. Almost certainly residents. Still a rule violation the property asked you to enforce.',go:'next'}, | |
| voice:{pts:5,txt:'It works, but always look first. A resident and a trespasser get very different tones.',go:'next'}, | |
| siren:{pts:-15,txt:'A siren at midnight over two residents at the pool. The property manager will hear about that from every neighbor.',go:'stay',fx:'siren'} }}, | |
| { kind:'alarm', title:'Residents after hours', cam:'CAM 02 POOL DECK', | |
| narr:'They are chatting, no alcohol visible, feet in the water. The site notes say residents get a courteous reminder.', | |
| scene:{actors:[{t:'person',x:196,y:112,c:'#d8895b'},{t:'person',x:212,y:112,c:'#7fb3d5'}]}, | |
| opts:{ voice:{pts:15,txt:'"Good evening, this is property security. The pool closed at 10. Please gather your things and head back to your unit. Thank you." They wave at the camera and go.',go:'next',fx:'voice'}, | |
| strobe:{pts:-10,txt:'Strobes on residents is the wrong register. A calm voice does this job.',go:'stay',fx:'strobe'}, | |
| guard:{pts:0,txt:'The courtesy officer could handle it, but a voice down resolves this in ten seconds and keeps him free for real calls.',go:'next'}, | |
| clear:{pts:-10,txt:'The client asked you to enforce pool hours. Give the reminder, then close it.',go:'stay'} }}, | |
| { kind:'patrol', title:'Scheduled patrol, rear gate', cam:'CAM 05 REAR GATE', | |
| narr:'Hourly camera tour. A figure at the rear pedestrian gate with something small at their feet.', | |
| scene:{actors:[{t:'person',x:250,y:114,c:'#9fbf8f',pose:'walk'},{t:'dog',x:238,y:124}]}, | |
| opts:{ watch:{pts:10,txt:'Resident walking a dog. He badges through the gate and it latches behind him.',go:'next'}, | |
| voice:{pts:-5,txt:'You nearly bark at a man walking his dog. Look first.',go:'stay'}, | |
| clear:{pts:0,txt:'Probably right, but you did not confirm the gate latched. Watch, then clear.',go:'stay'} }}, | |
| { kind:'patrol', title:'Resident confirmed', cam:'CAM 05 REAR GATE', | |
| narr:'Gate closed and latched. Resident and dog heading to building B.', | |
| scene:{actors:[{t:'person',x:150,y:114,c:'#9fbf8f',pose:'walk'},{t:'dog',x:138,y:124}]}, | |
| opts:{ clear:{pts:10,txt:'Patrol logged: resident with dog, badge access, gate secured.',go:'next'}, | |
| watch:{pts:5,txt:'Fine to watch him in. Then log the patrol.',go:'next'} }}, | |
| { kind:'alarm', title:'Person detected, breezeway', cam:'CAM 06 BREEZEWAY C', | |
| narr:'A man in the second floor breezeway pulling on apartment door handles, one after another.', | |
| scene:{actors:[{t:'person',x:120,y:80,c:'#3c3f4a',pose:'reach'}],breezeway:true}, | |
| opts:{ voice:{pts:15,txt:'"You on the second floor of building C in the dark jacket, you are on camera. Step away from the doors and leave the property." He spins around looking for the speaker.',go:'next',fx:'voice'}, | |
| police:{pts:5,txt:'Trying doors is real cause, but a voice down usually clears it faster than a patrol car. Warn first, then escalate if he stays.',go:'next'}, | |
| guard:{pts:5,txt:'The courtesy officer is a good next step. Warn the subject first so the officer is not walking into a surprise.',go:'next'}, | |
| clear:{pts:-15,txt:'A stranger trying doors at midnight is the opposite of clear.',go:'stay'} }}, | |
| { kind:'alarm', title:'Subject at the mail room', cam:'CAM 08 MAIL ROOM', | |
| narr:'He ducks into the stairwell, out of speaker range, and reappears at the mail room prying at the parcel lockers. This property has an on-site courtesy officer overnight.', | |
| scene:{actors:[{t:'person',x:96,y:112,c:'#3c3f4a',pose:'reach'}],spark:true}, | |
| opts:{ guard:{pts:15,txt:'You radio the courtesy officer: male, dark jacket, at the parcel lockers with a pry tool, alone. He heads over. You stay on the camera.',go:'next',fx:'guard'}, | |
| police:{pts:10,txt:'Solid. You also let the on-site officer know what is coming so nobody is surprised.',go:'next',fx:'police'}, | |
| voice:{pts:0,txt:'No speaker covers the mail room. He cannot hear you.',go:'stay'}, | |
| strobe:{pts:-5,txt:'No strobe in the mail room. Use the people you have on site.',go:'stay'} }}, | |
| { kind:'alarm', title:'Officer approaching', cam:'CAM 09 GARAGE ENTRY', | |
| narr:'The subject sees the officer coming and runs toward the parking garage. The officer keys the radio: where did he go?', | |
| scene:{actors:[{t:'person',x:60,y:112,c:'#3c3f4a',pose:'walk'},{t:'guard',x:200,y:116}]}, | |
| opts:{ watch:{pts:15,txt:'"Garage level one, running toward the north exit ramp." The officer cuts him off at the ramp and holds him. You call police, complete the incident report, and the property manager has video by morning.',go:'end'}, | |
| police:{pts:10,txt:'Right call, and you relay the garage location at the same time. Officers meet the courtesy officer at the north ramp.',go:'end',fx:'police'}, | |
| siren:{pts:-5,txt:'Not now. The officer needs to hear the radio. Tell him where the subject went.',go:'stay'} }}, | |
| ]}, | |
| { id:'build', name:'Construction site', sub:'Highway jobsite, 2:14 AM', cam:'CAM 02 EAST FENCE', steps:[ | |
| { kind:'alarm', title:'Perimeter alert, solar trailer', cam:'CAM 02 EAST FENCE', | |
| narr:'Movement along the east fence line. Low to the ground, moving slowly.', | |
| scene:{actors:[{t:'deer',x:230,y:120}]}, | |
| opts:{ watch:{pts:10,txt:'Thermal view shows a deer grazing along the fence. No person, no vehicle.',go:'next'}, | |
| siren:{pts:-10,txt:'You just sirened a deer. It ran off. The neighbors are awake.',go:'stay',fx:'siren'}, | |
| clear:{pts:0,txt:'Probably an animal, but verify. Thermal takes two seconds.',go:'stay'} }}, | |
| { kind:'alarm', title:'Wildlife confirmed', cam:'CAM 02 EAST FENCE', | |
| narr:'The deer wanders off toward the tree line.', | |
| scene:{actors:[{t:'deer',x:280,y:120}]}, | |
| opts:{ clear:{pts:10,txt:'Cleared as wildlife. Logged so the client sees why the alert count is what it is.',go:'next'}, | |
| watch:{pts:5,txt:'It is gone. Close the event with a note.',go:'next'} }}, | |
| { kind:'alarm', title:'Person detected, laydown yard', cam:'CAM 04 LAYDOWN YARD', | |
| narr:'Two subjects at the fence with bolt cutters, working toward the copper wire spools stacked inside.', | |
| scene:{actors:[{t:'person',x:246,y:116,c:'#4a4d5a',pose:'reach'},{t:'person',x:262,y:118,c:'#6b4f3a'}],spools:true}, | |
| opts:{ voice:{pts:15,txt:'"You two at the east fence, you are on camera and this site is being monitored. Drop the tools and leave now." One straightens up and steps back.',go:'next',fx:'voice'}, | |
| siren:{pts:10,txt:'Siren gets both of their heads up. Good, but the voice down telling them you can see the bolt cutters is what convinces them a person is watching.',go:'next',fx:'siren'}, | |
| police:{pts:5,txt:'Two people cutting a fence is real cause. Warn them first anyway; most leave and officers stay available.',go:'next'}, | |
| guard:{pts:-5,txt:'Remote site, no guard. Solar and cellular is the whole reason you are watching it.',go:'stay'} }}, | |
| { kind:'alarm', title:'One backs off, one keeps cutting', cam:'CAM 04 LAYDOWN YARD', | |
| narr:'The second subject looks toward the camera, then goes back to cutting.', | |
| scene:{actors:[{t:'person',x:246,y:116,c:'#4a4d5a',pose:'reach'},{t:'person',x:280,y:118,c:'#6b4f3a'}],spools:true,spark:true}, | |
| opts:{ strobe:{pts:15,txt:'The trailer strobes light up the whole yard. He stops cutting and shields his eyes.',go:'next',fx:'strobe'}, | |
| siren:{pts:15,txt:'Siren on. He stops cutting and looks up and down the road.',go:'next',fx:'siren'}, | |
| voice:{pts:5,txt:'Second warning. He does not care. Escalate.',go:'stay',fx:'voice'}, | |
| police:{pts:10,txt:'Fair. Fire the deterrents too; they buy time before officers arrive.',go:'next'} }}, | |
| { kind:'alarm', title:'Subjects inside the fence', cam:'CAM 04 LAYDOWN YARD', | |
| narr:'They are through. One loads a copper spool into a wheelbarrow while the other watches the road.', | |
| scene:{actors:[{t:'person',x:190,y:116,c:'#4a4d5a',pose:'reach'},{t:'person',x:270,y:118,c:'#6b4f3a'}],spools:true,barrow:true}, | |
| opts:{ police:{pts:15,txt:'Sheriff dispatch on the line: two males inside the east fence, one dark jacket, one brown, wheelbarrow, copper spools, bolt cutters on scene. Deputies are rolling.',go:'next',fx:'police'}, | |
| voice:{pts:0,txt:'They heard you the first two times. Officers now.',go:'stay'}, | |
| strobe:{pts:0,txt:'Already lit. They have decided to stay. Officers now.',go:'stay',fx:'strobe'}, | |
| clear:{pts:-15,txt:'Two people are stealing copper on your screen.',go:'stay'} }}, | |
| { kind:'alarm', title:'Deputies three minutes out', cam:'CAM 05 EQUIPMENT ROW', | |
| narr:'The subjects push the wheelbarrow behind the excavator, out of Cam 4. Cam 5 still has them. Dispatch asks for a location.', | |
| scene:{actors:[{t:'person',x:96,y:118,c:'#4a4d5a',pose:'walk'},{t:'person',x:112,y:118,c:'#6b4f3a',pose:'walk'}],barrow:true,excav:true}, | |
| opts:{ watch:{pts:15,txt:'"Behind the excavator, north side of the equipment row, both subjects, wheelbarrow with them. Best approach is the north gate." Deputies come in from the north and detain both. Client gets the video and the incident report before the crew arrives at six.',go:'end'}, | |
| siren:{pts:-5,txt:'Deputies are close. Give them the location, not more noise.',go:'stay'}, | |
| police:{pts:5,txt:'Already en route. What they need is your eyes. You relay the north side of the excavator and both are detained.',go:'end'} }}, | |
| ]}, | |
| { id:'storage', name:'Self storage', sub:'After-hours gate, 12:31 AM', cam:'CAM 01 MAIN GATE', steps:[ | |
| { kind:'patrol', title:'Scheduled patrol, main gate', cam:'CAM 01 MAIN GATE', | |
| narr:'Hourly camera tour. Gate closed, keypad idle, drive lane empty. Access hours ended at 10 PM.', | |
| scene:{actors:[]}, | |
| opts:{ clear:{pts:10,txt:'Patrol logged, all secure. The tour is the whole point: an open gate at 1 AM gets caught on the next one.',go:'next'}, | |
| watch:{pts:5,txt:'Nothing moving. Log the patrol and move on.',go:'next'}, | |
| voice:{pts:-5,txt:'Nobody to talk to. Log the patrol.',go:'stay'} }}, | |
| { kind:'alarm', title:'Vehicle at the gate', cam:'CAM 01 MAIN GATE', | |
| narr:'A sedan pulls up to the keypad. The driver enters a code. The gate does not move. He tries again.', | |
| scene:{actors:[{t:'car',x:150,y:126,c:'#8e3a3a'},{t:'person',x:174,y:112,c:'#7a7f8a',pose:'reach'}]}, | |
| opts:{ watch:{pts:10,txt:'Two rejected codes. Could be a tenant who forgot access hours, could be someone guessing. He sits in the car for a minute.',go:'next'}, | |
| voice:{pts:0,txt:'You remind him access hours ended at 10. Fair, but you have not confirmed anything yet.',go:'next',fx:'voice'}, | |
| clear:{pts:-10,txt:'Two failed codes after hours. Watch what he does next.',go:'stay'} }}, | |
| { kind:'alarm', title:'Vehicle leaving', cam:'CAM 01 MAIN GATE', | |
| narr:'He backs out of the lane and drives off. The gate never opened.', | |
| scene:{actors:[{t:'car',x:40,y:126,c:'#8e3a3a'}]}, | |
| opts:{ clear:{pts:10,txt:'Cleared with a note: vehicle attempted entry, two rejected codes, departed without access. Plate captured for the manager.',go:'next'}, | |
| watch:{pts:5,txt:'He is gone. Close it with the plate noted.',go:'next'} }}, | |
| { kind:'alarm', title:'Person detected, Row C', cam:'CAM 04 ROW C', | |
| narr:'Forty minutes later. A person on foot inside Row C. No vehicle came through the gate.', | |
| scene:{actors:[{t:'person',x:140,y:114,c:'#3f4451',pose:'walk'}]}, | |
| opts:{ watch:{pts:10,txt:'He is checking padlocks unit by unit and glancing back toward the fence. Nobody with a legitimate unit walks Row C at 1 AM without a car.',go:'next'}, | |
| voice:{pts:5,txt:'It will work, but confirm what he is doing first so your warning names it.',go:'next'}, | |
| clear:{pts:-15,txt:'A person inside the fence with no gate entry. That is a climb-over.',go:'stay'} }}, | |
| { kind:'alarm', title:'Subject cutting a lock', cam:'CAM 04 ROW C', | |
| narr:'He kneels at unit C-18 with bolt cutters.', | |
| scene:{actors:[{t:'person',x:140,y:114,c:'#3f4451',pose:'crouch'}],spark:true}, | |
| opts:{ voice:{pts:15,txt:'"You at the unit in the dark jacket, you are on camera and police are being notified. Put the tool down and leave the property now." He stands and looks around.',go:'next',fx:'voice'}, | |
| strobe:{pts:10,txt:'Strobes get him on his feet. Follow with the voice down so he knows a person is watching.',go:'next',fx:'strobe'}, | |
| police:{pts:10,txt:'Cutting a lock is real cause. Warn him at the same time; he may leave before officers arrive.',go:'next'}, | |
| guard:NOGUARD }}, | |
| { kind:'alarm', title:'Subject inside the unit', cam:'CAM 04 ROW C', | |
| narr:'He ignores it, snaps the lock and rolls the door up. He starts pulling boxes to the lane.', | |
| scene:{actors:[{t:'person',x:140,y:114,c:'#3f4451',pose:'reach'}],openUnit:true}, | |
| opts:{ police:{pts:15,txt:'Dispatch: burglary in progress, single male, dark jacket, inside a unit in Row C, came over the fence on foot. You stay on the line.',go:'next',fx:'police'}, | |
| siren:{pts:5,txt:'Siren on. He keeps loading. Officers now.',go:'stay',fx:'siren'}, | |
| voice:{pts:0,txt:'He heard you. Officers now.',go:'stay'}, | |
| clear:{pts:-15,txt:'A unit is open and being emptied on your screen.',go:'stay'} }}, | |
| { kind:'alarm', title:'Officers at the gate', cam:'CAM 01 MAIN GATE', | |
| narr:'A unit arrives at the main gate. You open it remotely. The subject hears the car and ducks back inside C-18.', | |
| scene:{actors:[{t:'police',x:120,y:126}]}, | |
| opts:{ watch:{pts:15,txt:'"Row C, third building on the left, unit 18, door is up and he is inside it." Officers take him at the door. The manager gets the video, the tenant gets a call in the morning instead of a surprise.',go:'end'}, | |
| police:{pts:5,txt:'They are here. Guide them: Row C, unit 18, door up.',go:'end'}, | |
| strobe:{pts:-5,txt:'Not now. Officers need to see. Tell them which unit.',go:'stay'} }}, | |
| ]}, | |
| { id:'retail', name:'Retail center', sub:'Strip center lot, 3:05 AM', cam:'CAM 06 REAR DOCK', steps:[ | |
| { kind:'alarm', title:'Motion, rear loading area', cam:'CAM 06 REAR DOCK', | |
| narr:'Motion at the dumpster enclosure behind the restaurant.', | |
| scene:{actors:[{t:'raccoon',x:236,y:78}],rear:true}, | |
| opts:{ watch:{pts:10,txt:'A raccoon on the dumpster lid. Third night this week.',go:'next'}, | |
| strobe:{pts:-5,txt:'The raccoon is unimpressed.',go:'stay',fx:'strobe'}, | |
| clear:{pts:0,txt:'Probably an animal, but look. It takes two seconds.',go:'stay'} }}, | |
| { kind:'alarm', title:'Wildlife confirmed', cam:'CAM 06 REAR DOCK', | |
| narr:'The raccoon drops into the enclosure and out of view.', | |
| scene:{actors:[],rear:true}, | |
| opts:{ clear:{pts:10,txt:'Cleared as wildlife. You add a note suggesting the enclosure gate get a latch; fewer alerts helps everyone.',go:'next'}, | |
| watch:{pts:5,txt:'Gone. Close it out.',go:'next'} }}, | |
| { kind:'alarm', title:'Vehicle, front lot', cam:'CAM 02 FRONT LOT', | |
| narr:'A car parks at the far end of the lot with its headlights off. Two occupants. Every store has been closed for hours.', | |
| scene:{actors:[{t:'car',x:262,y:132,c:'#2a2d3a'}]}, | |
| opts:{ watch:{pts:10,txt:'They sit for two minutes. Then the passenger gets out with a backpack and walks along the storefronts toward the pharmacy.',go:'next'}, | |
| voice:{pts:0,txt:'A car in a lot is not yet a reason to key the speaker. Watch what they do.',go:'stay'}, | |
| clear:{pts:-10,txt:'Lights off, two occupants, 3 AM. Keep watching.',go:'stay'} }}, | |
| { kind:'alarm', title:'Subject at the pharmacy door', cam:'CAM 03 STOREFRONTS', | |
| narr:'He crouches at the pharmacy entrance with a pry bar against the frame.', | |
| scene:{actors:[{t:'person',x:118,y:112,c:'#2f3340',pose:'crouch'},{t:'car',x:262,y:132,c:'#2a2d3a'}],spark:true}, | |
| opts:{ voice:{pts:15,txt:'"You at the pharmacy door with the backpack, you are on camera and police are being notified. Step away from the door." He stands up fast.',go:'next',fx:'voice'}, | |
| strobe:{pts:5,txt:'Strobes light the sidewalk. Add the voice so he knows a person is watching.',go:'next',fx:'strobe'}, | |
| police:{pts:10,txt:'A pry bar on a pharmacy door is real cause. Warn him as well; if he runs now the pharmacy stays intact.',go:'next'}, | |
| guard:NOGUARD }}, | |
| { kind:'alarm', title:'Driver signals', cam:'CAM 03 STOREFRONTS', | |
| narr:'The driver flashes the headlights twice. The subject looks back at the car, then goes back to the door frame.', | |
| scene:{actors:[{t:'person',x:118,y:112,c:'#2f3340',pose:'crouch'},{t:'car',x:262,y:132,c:'#2a2d3a',lights:true}],spark:true}, | |
| opts:{ siren:{pts:15,txt:'Siren echoes off the storefronts. He drops the bar and sprints for the car.',go:'next',fx:'siren'}, | |
| strobe:{pts:15,txt:'Strobes and the whole sidewalk goes white. He grabs the bar and runs for the car.',go:'next',fx:'strobe'}, | |
| voice:{pts:5,txt:'Second warning. He is committed. Escalate.',go:'stay',fx:'voice'}, | |
| police:{pts:10,txt:'Officers rolling. Hit the deterrents too; the goal is to make him leave before the glass breaks.',go:'next'} }}, | |
| { kind:'alarm', title:'Vehicle circling', cam:'CAM 06 REAR DOCK', | |
| narr:'He gets in but the car does not leave. It rolls to the rear of the building and idles behind the restaurant with the lights off.', | |
| scene:{actors:[{t:'car',x:150,y:126,c:'#2a2d3a'}],rear:true}, | |
| opts:{ police:{pts:15,txt:'Dispatch: attempted burglary at the pharmacy, two subjects now in a dark sedan idling behind the building, plate read off Cam 2. Officers en route.',go:'next',fx:'police'}, | |
| clear:{pts:-15,txt:'They tried a door and are still here. Officers.',go:'stay'}, | |
| voice:{pts:0,txt:'No speaker on the rear dock. Officers now.',go:'stay'} }}, | |
| { kind:'alarm', title:'Officers staging', cam:'CAM 06 REAR DOCK', | |
| narr:'Two units stage at the front entrance and ask where the vehicle is.', | |
| scene:{actors:[{t:'car',x:150,y:126,c:'#2a2d3a'},{t:'police',x:20,y:130}],rear:true}, | |
| opts:{ watch:{pts:15,txt:'"Behind the building, west end, dark sedan, engine running, two inside." Units come in from both ends of the service drive. Two in custody, pry bar recovered, pharmacy door intact.',go:'end'}, | |
| police:{pts:5,txt:'They are on scene. Give them the location: rear service drive, west end.',go:'end'}, | |
| siren:{pts:-5,txt:'Not now. Give officers the vehicle location.',go:'stay'} }}, | |
| ]}, | |
| { id:'hoa', name:'Gated community', sub:'HOA entrance, 1:12 AM', cam:'CAM 03 CLUBHOUSE', steps:[ | |
| { kind:'alarm', title:'Motion, clubhouse door', cam:'CAM 03 CLUBHOUSE', | |
| narr:'A figure at the clubhouse side door after hours.', | |
| scene:{actors:[{t:'person',x:96,y:112,c:'#c9a0c0',pose:'reach'}],club:true}, | |
| opts:{ watch:{pts:10,txt:'Resident with a gym bag using her fob. The fitness room is 24 hour access; the reader logs her unit number.',go:'next'}, | |
| voice:{pts:-5,txt:'You nearly challenged a resident going to the gym. Check the access log first.',go:'stay'}, | |
| clear:{pts:0,txt:'Likely fine, but confirm the fob read before you close it.',go:'stay'} }}, | |
| { kind:'alarm', title:'Resident confirmed', cam:'CAM 03 CLUBHOUSE', | |
| narr:'Door closes behind her. Fitness room lights come on.', | |
| scene:{actors:[],club:true,gymLit:true}, | |
| opts:{ clear:{pts:10,txt:'Cleared: resident, valid fob, fitness room. Logged.',go:'next'}, | |
| watch:{pts:5,txt:'She is in. Close it.',go:'next'} }}, | |
| { kind:'alarm', title:'Tailgate, entrance gate', cam:'CAM 01 ENTRY GATE', | |
| narr:'A car follows a resident\'s vehicle through the entrance gate before the arm drops. No transponder read on the second car.', | |
| scene:{actors:[{t:'car',x:100,y:134,c:'#e6e6e6'},{t:'car',x:150,y:134,c:'#2a2d3a'}],gate:true}, | |
| opts:{ watch:{pts:10,txt:'Plate captured. The second car turns into the first cul-de-sac and creeps along at walking pace with the lights off.',go:'next'}, | |
| voice:{pts:0,txt:'Tailgating happens with visitors and food delivery. Watch where it goes before you speak.',go:'stay'}, | |
| clear:{pts:-10,txt:'An unregistered vehicle just followed someone in at 1 AM. Track it.',go:'stay'} }}, | |
| { kind:'alarm', title:'Subjects on foot', cam:'CAM 05 CUL-DE-SAC', | |
| narr:'The car stops. Two people get out and start pulling door handles on the cars parked in driveways.', | |
| scene:{actors:[{t:'car',x:230,y:138,c:'#2a2d3a'},{t:'person',x:36,y:114,c:'#3a3d48',pose:'reach'},{t:'person',x:116,y:114,c:'#514238',pose:'reach'}],street:true}, | |
| opts:{ voice:{pts:15,txt:'"You two in the driveways, this is a private community and you are on camera. Leave now." One jogs back to the car.',go:'next',fx:'voice'}, | |
| police:{pts:10,txt:'Real cause. Warn them too; a voice down often empties the street before the first unit arrives.',go:'next'}, | |
| guard:{pts:5,txt:'This community has a roving patrol vehicle overnight. Good to alert them, but warn the subjects first.',go:'next'} }}, | |
| { kind:'alarm', title:'One keeps going', cam:'CAM 05 CUL-DE-SAC', | |
| narr:'The second subject finds an unlocked SUV and climbs into the front seat. Interior light comes on.', | |
| scene:{actors:[{t:'car',x:230,y:138,c:'#2a2d3a'},{t:'car',x:100,y:124,c:'#5a5a5a',lit:true}],street:true}, | |
| opts:{ police:{pts:15,txt:'Dispatch: vehicle entering in progress, one subject inside a gray SUV in a driveway, second subject in a dark sedan at the curb, plate provided. Officers en route.',go:'next',fx:'police'}, | |
| guard:{pts:10,txt:'Roving patrol notified with the location. You also call police; the patrol officer observes and does not engage.',go:'next',fx:'guard'}, | |
| siren:{pts:5,txt:'The gate pole siren carries across the neighborhood. He keeps going. Officers now.',go:'stay',fx:'siren'}, | |
| clear:{pts:-15,txt:'A man is inside a resident\'s vehicle. Officers.',go:'stay'} }}, | |
| { kind:'alarm', title:'Officers at the gate', cam:'CAM 01 ENTRY GATE', | |
| narr:'Two units arrive at the entrance. You raise the arm remotely. The sedan starts moving toward the back of the community.', | |
| scene:{actors:[{t:'police',x:110,y:134}],gate:true}, | |
| opts:{ watch:{pts:15,txt:'"Dark sedan heading to the rear exit gate, one subject on foot still in the cul-de-sac." One unit takes the exit, one takes the street. Both subjects stopped. The HOA board gets a clean report and video.',go:'end'}, | |
| police:{pts:5,txt:'They are here. What they need is direction: rear exit gate, and one on foot.',go:'end'}, | |
| strobe:{pts:-5,txt:'Not now. Tell officers which way the car went.',go:'stay'} }}, | |
| ]}, | |
| { id:'yard', name:'Distribution yard', sub:'Trailer yard, 2:48 AM', cam:'CAM 01 TRUCK GATE', steps:[ | |
| { kind:'alarm', title:'Vehicle, truck gate', cam:'CAM 01 TRUCK GATE', | |
| narr:'A tractor pulls up to the truck gate. No trailer.', | |
| scene:{actors:[{t:'tractor',x:130,y:118}],gate:true}, | |
| opts:{ watch:{pts:10,txt:'Driver holds a badge to the reader and the gate rolls open. His truck number matches the inbound list the client sent for tonight.',go:'next'}, | |
| voice:{pts:-5,txt:'You nearly challenged a scheduled driver. Check the inbound list first.',go:'stay'}, | |
| clear:{pts:0,txt:'Probably scheduled, but confirm the badge read.',go:'stay'} }}, | |
| { kind:'alarm', title:'Scheduled arrival', cam:'CAM 01 TRUCK GATE', | |
| narr:'He pulls through to the dock. Gate closes behind him.', | |
| scene:{actors:[],gate:true}, | |
| opts:{ clear:{pts:10,txt:'Cleared: scheduled inbound, badge verified, gate secured.',go:'next'}, | |
| watch:{pts:5,txt:'In and secured. Log it.',go:'next'} }}, | |
| { kind:'patrol', title:'Scheduled patrol, trailer rows', cam:'CAM 07 TRAILER ROW B', | |
| narr:'Hourly camera tour of the trailer rows. Something looks different from the last tour.', | |
| scene:{actors:[],rows:true,openTrailer:true,pickup:true}, | |
| opts:{ watch:{pts:10,txt:'Trailer in the middle of Row B has its doors open. It was sealed on the last patrol an hour ago. A pickup is backed up to it with the lights off.',go:'next'}, | |
| clear:{pts:-15,txt:'You would have logged an all clear with a trailer standing open. Look at each row.',go:'stay'} }}, | |
| { kind:'alarm', title:'Subjects at the trailer', cam:'CAM 07 TRAILER ROW B', | |
| narr:'One person inside the trailer handing boxes down to another on the ground, who loads them into the pickup bed.', | |
| scene:{actors:[{t:'person',x:196,y:88,c:'#3a3d48'},{t:'person',x:200,y:112,c:'#514238',pose:'reach'}],rows:true,openTrailer:true,pickup:true}, | |
| opts:{ voice:{pts:15,txt:'"You two at the open trailer in Row B, you are on camera and police are being notified. Stop and leave the property." Both freeze.',go:'next',fx:'voice'}, | |
| police:{pts:10,txt:'Cargo theft in progress is real cause. Warn them at the same time; the boxes still on the ground stay on the ground.',go:'next'}, | |
| guard:{pts:-5,txt:'No guard on this yard overnight. That is why the client has you.',go:'stay'} }}, | |
| { kind:'alarm', title:'Loading faster', cam:'CAM 07 TRAILER ROW B', | |
| narr:'They look at each other and start loading faster.', | |
| scene:{actors:[{t:'person',x:196,y:88,c:'#3a3d48'},{t:'person',x:200,y:112,c:'#514238',pose:'reach'}],rows:true,openTrailer:true,pickup:true}, | |
| opts:{ strobe:{pts:15,txt:'Strobes on the light pole flood Row B. The one on the ground drops a box and jumps in the truck.',go:'next',fx:'strobe'}, | |
| siren:{pts:15,txt:'Siren carries across the whole yard. They abandon the boxes and get in the truck.',go:'next',fx:'siren'}, | |
| voice:{pts:5,txt:'Second warning. They are racing you now. Escalate.',go:'stay',fx:'voice'}, | |
| police:{pts:10,txt:'Officers rolling. Light them up too; every box they leave behind is the client\'s.',go:'next'} }}, | |
| { kind:'alarm', title:'Pickup moving', cam:'CAM 09 REAR GATE', | |
| narr:'The pickup drives to the rear gate. It is chained. The driver gets out with bolt cutters.', | |
| scene:{actors:[{t:'pickup',x:120,y:122},{t:'person',x:170,y:112,c:'#3a3d48',pose:'reach'}],gate:true,rear:true}, | |
| opts:{ police:{pts:15,txt:'Dispatch: cargo theft, two subjects, dark pickup with boxes in the bed, cutting the chain on the rear gate on the north side, plate provided. Officers two minutes out.',go:'next',fx:'police'}, | |
| voice:{pts:0,txt:'No speaker covers the rear gate. Officers now.',go:'stay'}, | |
| strobe:{pts:0,txt:'No strobe at the rear gate. Officers now.',go:'stay'}, | |
| clear:{pts:-15,txt:'They are cutting their way out with the client\'s freight.',go:'stay'} }}, | |
| { kind:'alarm', title:'Officers arriving', cam:'CAM 09 REAR GATE', | |
| narr:'Officers ask which gate. The chain is half cut.', | |
| scene:{actors:[{t:'pickup',x:120,y:122},{t:'person',x:170,y:112,c:'#3a3d48',pose:'reach'},{t:'police',x:10,y:134}],gate:true,rear:true}, | |
| opts:{ watch:{pts:15,txt:'"Rear gate, north side, off the service road. Pickup is nose in against the gate, one subject cutting, one in the truck." Officers block the service road. Both in custody, freight recovered from the truck bed.',go:'end'}, | |
| police:{pts:5,txt:'They are close. Guide them: rear gate on the north service road.',go:'end'}, | |
| siren:{pts:-5,txt:'Not now. Tell officers which gate.',go:'stay'} }}, | |
| ]}, | |
| ]; | |
| /* ============ Integrity check ============ */ | |
| SCENARIOS.forEach(s=>{ | |
| s.max=s.steps.reduce((a,st)=>a+Math.max(...Object.values(st.opts).filter(o=>o.go&&o.go!=='stay').map(o=>o.pts)),0); | |
| s.steps.forEach((st,i)=>{ const ex=Object.values(st.opts).some(o=>o.go==='next'||o.go==='end'); if(!ex)console.warn('Step has no exit',s.id,i); if(i===s.steps.length-1&&!Object.values(st.opts).some(o=>o.go==='end'))console.warn('Last step has no end',s.id); }); | |
| }); | |
| /* ============ Pixel renderer ============ */ | |
| const cv=document.getElementById('cv'); let ctx=cv.getContext('2d'); ctx.imageSmoothingEnabled=false; | |
| const W=320,H=180; | |
| let current=null, stepIdx=0, score=0, recap=[], fx={type:null,t0:0}, sceneOverride=null, lastPick=null; | |
| const reduced=matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| function px(x,y,w,h,c){ctx.fillStyle=c;ctx.fillRect(x|0,y|0,w,h);} | |
| function starfield(seed){ let s=seed; for(let i=0;i<40;i++){ s=(s*9301+49297)%233280; const x=(s/233280)*W; s=(s*9301+49297)%233280; const y=(s/233280)*70; px(x,y,1,1,(i%5==0)?'#d9e6ff':'#6c7aa8'); } } | |
| function sky(){ px(0,0,W,40,'#0b0f2a');px(0,40,W,30,'#101740');px(0,70,W,40,'#182052'); starfield(7); px(270,16,7,7,'#e8e6c8');px(272,15,3,1,'#e8e6c8');px(272,23,3,1,'#e8e6c8');px(269,18,1,3,'#e8e6c8');px(277,18,1,3,'#e8e6c8'); } | |
| function pole(x,base){ px(x,base-70,2,70,'#2a2f45'); px(x-6,base-72,14,3,'#3b4160'); px(x-4,base-69,10,2,'#f6e7a1'); ctx.fillStyle='rgba(246,231,161,0.06)'; ctx.beginPath(); ctx.moveTo(x-4,base-68); ctx.lineTo(x-40,base); ctx.lineTo(x+44,base); ctx.lineTo(x+6,base-68); ctx.fill(); } | |
| function speaker(x,y){ px(x,y,6,8,'#3b4160'); px(x+1,y+1,4,6,'#1c2038'); px(x+6,y+2,2,4,'#00a9a5'); } | |
| function camIcon(x,y){ px(x,y,8,5,'#2f3652');px(x+8,y+1,3,3,'#1c2038');px(x+3,y+5,2,3,'#2f3652'); px(x+9,y+2,1,1,'#e0483e'); } | |
| function fence(y,h){ for(let x=0;x<W;x+=6)px(x,y,1,h,'#a8adb8'); px(0,y,W,1,'#a8adb8'); px(0,(y+h/2)|0,W,1,'#a8adb8'); px(0,y+h-1,W,1,'#a8adb8'); for(let x=0;x<W;x+=24)px(x,y-3,2,h+3,'#7c8290'); } | |
| function car(x,y,c,o={}){ px(x+2,y,16,4,c);px(x,y+3,20,5,c);px(x+3,y+1,5,2,o.lit?'#f6e7a1':'#9fd3e8');px(x+10,y+1,5,2,o.lit?'#f6e7a1':'#9fd3e8');px(x+2,y+8,4,3,'#111');px(x+14,y+8,4,3,'#111');px(x+3,y+9,2,1,'#666');px(x+15,y+9,2,1,'#666'); if(o.lights){px(x-1,y+4,2,2,'#fff7c2'); ctx.fillStyle='rgba(255,247,194,.12)'; ctx.beginPath(); ctx.moveTo(x,y+4); ctx.lineTo(x-40,y-6); ctx.lineTo(x-40,y+16); ctx.fill();} } | |
| function truck(x,y,c){ px(x+12,y-2,10,6,c);px(x,y+2,24,6,c);px(x+14,y-1,6,3,'#9fd3e8');px(x+1,y+3,10,3,'rgba(0,0,0,.3)');px(x+3,y+8,4,3,'#111');px(x+16,y+8,4,3,'#111'); } | |
| function pickup(x,y){ px(x+16,y-3,12,7,'#1f2230');px(x,y+2,30,7,'#1f2230');px(x+18,y-2,8,4,'#9fd3e8');px(x+2,y+1,12,3,'#8a5a2b');px(x+4,y+9,5,4,'#111');px(x+22,y+9,5,4,'#111'); } | |
| function tractor(x,y){ px(x,y-8,20,14,'#b23a3a');px(x+2,y-6,10,6,'#9fd3e8');px(x+20,y,8,6,'#2b2b33');px(x+3,y+6,6,5,'#111');px(x+14,y+6,6,5,'#111');px(x+22,y+6,6,5,'#111');px(x+8,y-14,2,6,'#888'); } | |
| function trailer(x,y,open){ px(x,y,44,22,'#d6d9de');px(x,y,44,2,'#a8adb8');px(x+2,y+22,6,4,'#111');px(x+10,y+22,6,4,'#111');px(x+40,y+22,2,4,'#555'); if(open){px(x+34,y+2,10,20,'#1c1f2e');px(x+30,y+4,4,16,'#a8adb8');} } | |
| function person(a,t){ const {x,y}=a, c=a.c||'#555'; const pose=a.pose||'stand'; const bob=(pose==='walk')?((Math.floor(t/220)%2)?1:0):0; | |
| if(pose==='crouch'){ px(x,y+4,6,4,c);px(x+1,y+1,4,3,'#d9b48f');px(x,y+8,2,3,'#2b2b33');px(x+4,y+8,2,3,'#2b2b33');px(x+6,y+5,4,2,c); return; } | |
| px(x+1,y-bob,4,4,'#d9b48f'); px(x,y+4-bob,6,7,c); px(x,y+11-bob,2,5,'#2b2b33'); px(x+4,y+11-bob,2,5,'#2b2b33'); | |
| if(pose==='reach'){ px(x+6,y+5,4,2,c); px(x+10,y+5,2,2,'#d9b48f'); } | |
| else if(pose==='walk'){ px(x-2+bob*2,y+6,2,2,c); px(x+6-bob*2,y+6,2,2,c); } | |
| else { px(x-2,y+5,2,5,c); px(x+6,y+5,2,5,c); } } | |
| function guardSprite(a,t){ person({...a,c:'#1b2a4a'},t); px(a.x+1,a.y-3,4,2,'#1b2a4a'); px(a.x+2,a.y+6,2,2,'#f2b632'); } | |
| function dog(a,t){ const w=(Math.floor(t/200)%2); px(a.x,a.y,8,4,'#8a6a4a');px(a.x+7,a.y-2,4,3,'#8a6a4a');px(a.x+1,a.y+4,1,2+w,'#5a4330');px(a.x+6,a.y+4,1,3-w,'#5a4330');px(a.x-2,a.y-1,2,2,'#8a6a4a'); } | |
| function deer(a,t){ const w=(Math.floor(t/400)%2); px(a.x,a.y,12,6,'#a9825a');px(a.x+11,a.y-5,4,6,'#a9825a');px(a.x+12,a.y-8,1,3,'#6e5238');px(a.x+14,a.y-8,1,3,'#6e5238');px(a.x+1,a.y+6,2,5-w,'#7a5a3c');px(a.x+9,a.y+6,2,5,'#7a5a3c');px(a.x+5,a.y+6,2,5,'#7a5a3c'); } | |
| function raccoon(a,t){ const w=(Math.floor(t/300)%2); px(a.x,a.y,10,5,'#6e6e78');px(a.x+9,a.y-1,4,4,'#6e6e78');px(a.x+10,a.y,3,1,'#222');px(a.x-4,a.y+1,4,2,w?'#6e6e78':'#3a3a44');px(a.x-2,a.y+1,2,2,'#3a3a44');px(a.x+1,a.y+5,1,2,'#3a3a44');px(a.x+7,a.y+5,1,2,'#3a3a44'); } | |
| function tow(a){ const {x,y}=a; px(x+30,y-2,12,6,'#e0a72f');px(x,y+3,44,6,'#e0a72f');px(x+2,y-1,26,5,'#3b4160');px(x+6,y+1,14,2,'#8c8c8c');px(x+32,y-1,7,3,'#9fd3e8');px(x+4,y+9,4,3,'#111');px(x+20,y+9,4,3,'#111');px(x+36,y+9,4,3,'#111'); car(x+6,y-6,'#c94a4a'); } | |
| function policeCar(a,t){ const {x,y}=a; car(x,y,'#e6e6e6'); px(x+2,y+5,16,2,'#1e3a8a'); px(x+7,y-2,6,2,(Math.floor(t/150)%2)?'#3b82f6':'#ef4444'); if(!reduced){ ctx.fillStyle=(Math.floor(t/150)%2)?'rgba(59,130,246,.12)':'rgba(239,68,68,.12)'; ctx.fillRect(0,0,W,H);} } | |
| function barrow(x,y){ px(x,y,10,5,'#5c6470');px(x+10,y+1,4,1,'#5c6470');px(x+2,y+5,3,3,'#222');px(x+1,y-3,8,3,'#b5622c'); } | |
| function spools(x,y){ for(let i=0;i<3;i++){ px(x+i*14,y,12,12,'#8a5a2b');px(x+2+i*14,y+2,8,8,'#b5622c');px(x+5+i*14,y+5,2,2,'#3a2412'); } } | |
| const SCENES={ | |
| dealer(sc){ sky(); px(0,110,W,70,'#2c2f39'); px(0,110,W,2,'#3f4352'); | |
| px(20,58,120,52,'#1f2544'); px(24,64,112,40,'#3a5a7a'); for(let i=0;i<7;i++)px(28+i*16,66,12,36,'#6f9ec2'); px(20,54,120,4,'#00a9a5'); px(50,46,60,8,'#e6ebf1'); px(54,48,52,4,'#272f4f'); | |
| px(0,88,W,2,'#4a4f66'); for(let i=0;i<16;i++)px(i*20,84,2,6,'#4a4f66'); | |
| const cols=['#c94a4a','#e6e6e6','#3b6fb6','#5a5a5a','#e6e6e6','#8e3a3a','#2f7a5a','#e6e6e6']; | |
| for(let i=0;i<8;i++) truck(10+i*40,128,cols[i]); for(let i=0;i<7;i++) car(30+i*42,150,cols[(i+3)%8]); | |
| pole(180,110); pole(300,110); speaker(186,44); camIcon(140,40); | |
| if((sc.cam||'').startsWith('CAM 05')){ px(200,60,120,50,'#252a45'); for(let i=0;i<3;i++)px(206+i*38,70,30,40,'#151830'); px(200,56,120,4,'#3a4062'); } }, | |
| multi(sc){ sky(); px(0,112,W,68,'#3a4a3a'); px(0,112,W,2,'#4c6048'); | |
| px(10,40,150,72,'#4b3f36'); px(10,40,150,3,'#2f2822'); for(let r=0;r<3;r++)for(let i=0;i<6;i++){ const lit=((r*7+i*3)%5)<2; px(16+i*24,46+r*22,14,12,lit?'#f6d98a':'#1c1f36'); } | |
| for(let r=0;r<3;r++)px(10,58+r*22,150,3,'#6b5a4c'); px(70,96,30,16,'#2a2420'); px(72,98,26,14,'#7fb3d5'); | |
| if(sc.breezeway){ px(10,68,150,2,'#e6ebf1'); for(let i=0;i<10;i++)px(12+i*15,62,1,6,'#e6ebf1'); } | |
| px(180,122,110,30,'#0e6e6c'); px(184,126,102,22,'#12a3a0'); px(178,120,114,2,'#cfd6dc'); px(178,152,114,2,'#cfd6dc'); px(180,118,2,20,'#cfd6dc');px(186,118,2,20,'#cfd6dc');px(180,120,8,1,'#cfd6dc'); | |
| px(170,112,6,10,'#7c8a99');px(150,110,4,20,'#c2c7cc'); for(let x=200;x<320;x+=8)px(x,90,2,22,'#3b3f4a'); px(200,90,120,2,'#3b3f4a'); px(200,100,120,1,'#3b3f4a'); | |
| const c=sc.cam||''; if(c.startsWith('CAM 05')){ px(240,86,3,26,'#4b4f5c');px(262,86,3,26,'#4b4f5c'); px(243,92,19,2,'#4b4f5c'); } | |
| if(c.startsWith('CAM 08')){ px(60,70,120,42,'#2b2f44'); for(let r=0;r<3;r++)for(let i=0;i<8;i++)px(64+i*14,74+r*12,11,9,'#4a5170'); } | |
| if(c.startsWith('CAM 09')){ px(20,60,200,52,'#2a2d3d'); px(30,70,80,42,'#0c0e1a'); px(120,70,80,42,'#0c0e1a'); px(20,56,200,4,'#f2b632'); } | |
| pole(300,112); speaker(150,56); camIcon(165,36); }, | |
| build(sc){ sky(); px(0,114,W,66,'#6e5a3e'); px(0,114,W,2,'#8a7050'); px(40,140,60,6,'#5a4a34'); px(180,160,90,5,'#5a4a34'); | |
| px(60,20,4,94,'#c9a227'); px(30,24,120,4,'#c9a227'); px(62,24,2,30,'#c9a227'); px(128,28,1,40,'#666'); px(124,68,10,6,'#888'); | |
| px(220,60,90,54,'#4f5866'); px(224,66,82,42,'#333a48'); px(226,70,10,10,'#f6d98a'); px(220,56,90,4,'#c9a227'); fence(92,22); | |
| if(sc.spools) spools(150,116); | |
| if(sc.excav){ px(20,96,60,18,'#e0a72f'); px(24,84,30,14,'#e0a72f'); px(28,88,14,8,'#9fd3e8'); px(70,70,6,30,'#e0a72f'); px(76,70,30,6,'#e0a72f'); px(100,76,10,12,'#7a6a3a'); px(16,114,68,8,'#333'); } | |
| if(sc.barrow&&sc.actors[0]) barrow(sc.actors[0].x-14,sc.actors[0].y+8); | |
| px(280,84,14,30,'#3b4160'); px(283,74,8,10,'#1f2a5a'); px(286,64,2,10,'#3b4160'); speaker(292,66); camIcon(276,60); }, | |
| storage(sc){ sky(); px(0,112,W,68,'#33363f'); px(0,112,W,2,'#4a4e5a'); px(0,140,W,1,'#f2b632'); | |
| if((sc.cam||'').startsWith('CAM 04')){ px(0,60,W,52,'#c9c2b0'); px(0,60,W,4,'#8a5a2b'); for(let i=0;i<9;i++){ const x=4+i*36; const open=sc.openUnit&&i===4; px(x,68,28,44,open?'#15171f':'#e07b2a'); if(!open)for(let r=0;r<10;r++)px(x,70+r*4,28,1,'#b8621e'); px(x+12,104,4,2,'#333'); } px(0,60,W,1,'#6e6e78'); } | |
| else { px(0,70,110,42,'#c9c2b0'); px(0,70,110,3,'#8a5a2b'); px(10,80,22,32,'#e07b2a'); px(40,80,22,32,'#e07b2a'); px(70,80,22,32,'#e07b2a'); | |
| px(210,70,110,42,'#c9c2b0'); px(210,70,110,3,'#8a5a2b'); px(220,80,22,32,'#e07b2a'); px(250,80,22,32,'#e07b2a'); px(280,80,22,32,'#e07b2a'); | |
| px(110,88,4,24,'#5a5f6e'); px(206,88,4,24,'#5a5f6e'); for(let x=114;x<206;x+=5)px(x,90,1,22,'#a8adb8'); px(114,90,92,1,'#a8adb8'); px(114,111,92,1,'#a8adb8'); | |
| px(120,100,6,12,'#3b4160'); px(121,102,4,4,'#0f6f6d'); } | |
| pole(300,112); speaker(150,50); camIcon(160,38); }, | |
| retail(sc){ sky(); | |
| if(sc.rear){ px(0,110,W,70,'#2c2f39'); px(0,110,W,2,'#3f4352'); px(0,50,W,60,'#5a5148'); px(0,50,W,4,'#3d362f'); px(40,80,24,30,'#2a2620'); px(120,80,24,30,'#2a2620'); px(200,72,60,38,'#4d5a3e'); px(206,78,48,30,'#3b4630'); px(210,86,44,18,'#2d5a35'); px(212,84,40,4,'#1f3a25'); pole(300,110); camIcon(20,44); } | |
| else { px(0,112,W,68,'#2c2f39'); px(0,112,W,2,'#3f4352'); for(let x=0;x<W;x+=40)px(x,140,20,1,'#f2b632'); for(let x=20;x<W;x+=40)px(x,164,20,1,'#f2b632'); | |
| px(0,56,W,56,'#8c7b6b'); px(0,52,W,4,'#5a4d42'); const aw=['#c94a4a','#2f7a5a','#3b6fb6','#e0a72f']; | |
| for(let i=0;i<4;i++){ const x=i*80; px(x+6,66,68,40,'#1c2038'); px(x+8,68,64,36,'#3a5a7a'); px(x+30,84,20,22,'#9fd3e8'); px(x+4,60,72,6,aw[i]); for(let s=0;s<6;s++)px(x+6+s*12,66,6,3,aw[i]); px(x+16,70,48,4,'#e6ebf1'); } | |
| px(110,84,20,22,'#f6d98a'); px(114,58,8,3,'#0f6f6d'); pole(300,112); speaker(150,50); camIcon(40,44); } }, | |
| hoa(sc){ sky(); px(0,112,W,68,'#2f3a2f'); px(0,112,W,2,'#4c6048'); | |
| const house=(x,c)=>{ px(x,80,50,32,c); px(x-4,80,58,4,'#3d362f'); ctx.fillStyle='#5a4d42'; ctx.beginPath(); ctx.moveTo(x-4,80); ctx.lineTo(x+25,62); ctx.lineTo(x+54,80); ctx.fill(); px(x+8,90,10,10,'#f6d98a'); px(x+30,90,10,10,'#1c1f36'); px(x+20,98,8,14,'#2a2420'); }; | |
| if(sc.gate){ px(0,130,W,30,'#3a3d48'); px(0,130,W,1,'#e6ebf1'); px(0,159,W,1,'#e6ebf1'); px(150,130,2,30,'#f2b632'); | |
| px(96,88,20,42,'#8a5a2b'); px(98,90,16,38,'#a86f3a'); px(94,84,24,4,'#3d362f'); px(210,88,20,42,'#8a5a2b'); px(212,90,16,38,'#a86f3a'); px(208,84,24,4,'#3d362f'); | |
| px(116,106,94,3,'#e6ebf1'); for(let i=0;i<8;i++)px(120+i*12,109,4,2,'#c94a4a'); px(120,64,80,22,'#e6ebf1'); px(124,68,72,14,'#272f4f'); px(60,102,8,28,'#5a5f6e'); px(62,106,4,4,'#0f6f6d'); } | |
| else if(sc.street){ house(10,'#b8a888'); house(90,'#8fa0b8'); house(170,'#c2b09a'); house(250,'#a0b898'); px(0,134,W,26,'#3a3d48'); px(0,146,W,1,'#f2b632'); for(let i=0;i<4;i++)px(20+i*80,112,24,22,'#4a4e5a'); } | |
| if(sc.club){ px(40,60,180,52,'#c2b09a'); px(36,56,188,4,'#3d362f'); px(60,80,30,32,'#2a2420'); px(100,70,40,30,sc.gymLit?'#f6d98a':'#1c1f36'); px(150,70,40,30,sc.gymLit?'#f6d98a':'#1c1f36'); px(54,60,152,6,'#00a9a5'); px(230,86,6,26,'#5a5f6e'); } | |
| pole(300,112); speaker(66,90); camIcon(30,40); }, | |
| yard(sc){ sky(); px(0,112,W,68,'#3a3d44'); px(0,112,W,2,'#50545e'); | |
| if(sc.rows){ px(0,60,W,52,'#2b2f3a'); for(let i=0;i<6;i++){ const x=6+i*52; trailer(x,84,sc.openTrailer&&i===3); } if(sc.pickup)pickup(150,118); pole(300,112); speaker(150,46); camIcon(20,40); } | |
| else { px(0,40,W,72,'#4a5060'); px(0,40,W,4,'#2e323e'); for(let i=0;i<6;i++){ px(10+i*52,80,36,32,'#1c1f2e'); px(12+i*52,82,32,4,'#f2b632'); } fence(92,22); px(120,90,80,22,'#7c8290'); if(sc.rear){ px(150,96,20,3,'#c9c9c9'); } else { px(150,60,20,30,'#3b4160'); px(154,64,12,10,'#f6d98a'); } pole(60,112); pole(260,112); speaker(66,46); camIcon(30,36); } }, | |
| }; | |
| function siteLabel(name,sub,thumb){ const t=name.toUpperCase(); | |
| if(thumb){ ctx.font='12px "Press Start 2P", monospace'; const w=ctx.measureText(t).width+24; px((W-w)/2,H-52,w,26,'rgba(13,16,36,.88)'); px((W-w)/2,H-52,w,3,'#00a9a5'); ctx.fillStyle='#e6ebf1'; ctx.fillText(t,(W-w)/2+12,H-33); } | |
| else { ctx.font='7px "Press Start 2P", monospace'; const w=ctx.measureText(t).width+10; px(W-w-4,H-12,w,9,'rgba(13,16,36,.75)'); ctx.fillStyle='#00a9a5'; ctx.fillText(t,W-w+1,H-5); } } | |
| function draw(t){ ctx.clearRect(0,0,W,H); | |
| const sc=sceneOverride||(current?current.steps[stepIdx].scene:{actors:[]}); | |
| const kind=sceneOverride?sceneOverride.kind:(current?current.id:'dealer'); | |
| const camName=sc.cam||(sceneOverride?sceneOverride.cam:(current?current.steps[stepIdx].cam:'CAM 01')); | |
| SCENES[kind]({...sc,cam:camName}); | |
| const el=fx.type?t-fx.t0:0; const active=fx.type&&el<2600; | |
| for(const a of sc.actors){ | |
| let aa=a; if(active&&fx.type!=='police'&&fx.type!=='guard'&&a.t==='person'&&!reduced){ aa={...a,pose:'stand'}; } | |
| switch(a.t){ | |
| case 'person': person(aa,t); if(!sceneOverride){ ctx.strokeStyle='#00a9a5'; ctx.lineWidth=1; ctx.strokeRect(a.x-3.5,a.y-3.5,13,(aa.pose==='crouch'?15:21)); const crowded=sc.actors.some(o=>o!==a&&o.t==='person'&&o.x<a.x&&a.x-o.x<34); if(!crowded){px(a.x-4,a.y-10,30,6,'#00a9a5'); ctx.fillStyle='#062322'; ctx.font='5px "Press Start 2P", monospace'; ctx.fillText('PERSON',a.x-2,a.y-5);} } break; | |
| case 'dog': dog(a,t); break; case 'deer': deer(a,t); break; case 'raccoon': raccoon(a,t); break; case 'tow': tow(a); break; | |
| case 'police': policeCar(a,t); break; case 'guard': guardSprite(a,t); break; | |
| case 'car': car(a.x,a.y,a.c,a); if(!sceneOverride){ctx.strokeStyle='#00a9a5';ctx.strokeRect(a.x-2.5,a.y-2.5,25,15);} break; | |
| case 'pickup': pickup(a.x,a.y); break; case 'tractor': tractor(a.x,a.y); break; | |
| } | |
| } | |
| if(sc.spark&&!active){ const p=sc.actors.find(a=>a.t==='person'); if(p&&Math.floor(t/90)%3===0)px(p.x+11,p.y+4,2,2,'#fff3b0'); } | |
| if(active){ | |
| if(fx.type==='strobe'){ const on=reduced?true:(Math.floor(el/80)%2===0); if(on){ctx.fillStyle=reduced?'rgba(255,255,255,.25)':'rgba(255,255,255,.75)';ctx.fillRect(0,0,W,H);} } | |
| if(fx.type==='siren'){ ctx.strokeStyle='#f2b632'; ctx.lineWidth=1; for(let i=0;i<3;i++){ const r=((el/8)+i*14)%50; ctx.beginPath(); ctx.arc(W/2,60,r,0,Math.PI*2); ctx.stroke(); } if(!reduced&&Math.floor(el/300)%2===0){ctx.fillStyle='rgba(242,182,50,.10)';ctx.fillRect(0,0,W,H);} } | |
| if(fx.type==='voice'){ px(W/2-40,8,80,18,'#e6ebf1'); px(W/2-6,26,4,3,'#e6ebf1'); ctx.fillStyle='#272f4f'; ctx.font='8px "Press Start 2P", monospace'; ctx.fillText('YOU ARE ON',W/2-36,16); ctx.fillText('CAMERA',W/2-20,24); } | |
| if(fx.type==='police'){ const x=-30+Math.min(el/2600,1)*140; policeCar({x,y:150},t); } | |
| if(fx.type==='guard'){ const x=W+10-Math.min(el/2600,1)*120; guardSprite({x,y:118},t); } | |
| } | |
| ctx.fillStyle='rgba(0,0,0,.45)'; ctx.fillRect(0,0,W,12); ctx.fillStyle='#e6ebf1'; ctx.font='7px "Press Start 2P", monospace'; | |
| ctx.fillText(camName,4,9); const rec=Math.floor(t/600)%2===0; if(rec||reduced){px(W-40,3,5,5,'#e0483e');} ctx.fillText('REC',W-32,9); | |
| const d=new Date(); ctx.fillText([d.getHours(),d.getMinutes(),d.getSeconds()].map(n=>String(n).padStart(2,'0')).join(':'),W-108,9); | |
| if(current&&!sceneOverride) siteLabel(current.name,current.sub,false); | |
| if(current&&!sceneOverride){ const k=current.steps[stepIdx].kind; ctx.fillStyle=k==='patrol'?'rgba(154,166,189,.85)':'rgba(224,72,62,.85)'; ctx.fillRect(4,H-12,k==='patrol'?46:40,9); ctx.fillStyle='#fff'; ctx.font='6px "Press Start 2P", monospace'; ctx.fillText(k==='patrol'?'PATROL':'ALARM',7,H-5); } | |
| requestAnimationFrame(draw); | |
| } | |
| /* ============ Sound (all synthesized, no files) ============ */ | |
| let muted=false, ac=null, noiseBuf=null, unlocked=false; | |
| /* Silent looping <audio> element: on iOS this moves the page to the "playback" audio session so WebAudio is heard even with the ringer switch on silent. */ | |
| const silentEl=document.createElement('audio'); | |
| silentEl.setAttribute('playsinline',''); silentEl.setAttribute('loop',''); silentEl.muted=false; silentEl.volume=0.01; | |
| silentEl.src='data:audio/wav;base64,UklGRmQfAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YUAfAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgA=='; | |
| function audio(){ if(!ac){ ac=new (window.AudioContext||window.webkitAudioContext)(); const len=ac.sampleRate; noiseBuf=ac.createBuffer(1,len,ac.sampleRate); const d=noiseBuf.getChannelData(0); for(let i=0;i<len;i++)d[i]=Math.random()*2-1; } if(ac.state!=='running')ac.resume(); return ac; } | |
| function unlock(){ if(unlocked)return; try{ const a=audio(); const b=a.createBuffer(1,1,22050),src=a.createBufferSource(); src.buffer=b; src.connect(a.destination); src.start(0); a.resume().then(()=>{ if(a.state==='running')unlocked=true; }); silentEl.play().catch(()=>{}); }catch(e){} } | |
| ['touchstart','touchend','pointerdown','mousedown','keydown'].forEach(ev=>document.addEventListener(ev,unlock,{capture:true,passive:true})); | |
| document.addEventListener('visibilitychange',()=>{ if(!document.hidden&&ac&&ac.state!=='running')ac.resume(); }); | |
| function tone(f,dur,{type='square',g=0.04,to=null,at=0,attack=0.005}={}){ if(muted)return; try{ const a=audio(),o=a.createOscillator(),v=a.createGain(),t0=a.currentTime+at; o.type=type;o.frequency.setValueAtTime(f,t0); if(to)o.frequency.linearRampToValueAtTime(to,t0+dur); v.gain.setValueAtTime(0.0001,t0); v.gain.linearRampToValueAtTime(g,t0+attack); v.gain.exponentialRampToValueAtTime(0.0001,t0+dur); o.connect(v).connect(a.destination); o.start(t0); o.stop(t0+dur+0.02);}catch(e){} } | |
| function noise(dur,{g=0.03,at=0,hp=800,lp=4000}={}){ if(muted)return; try{ const a=audio(),s=a.createBufferSource(),v=a.createGain(),h=a.createBiquadFilter(),l=a.createBiquadFilter(),t0=a.currentTime+at; s.buffer=noiseBuf; h.type='highpass';h.frequency.value=hp; l.type='lowpass';l.frequency.value=lp; v.gain.setValueAtTime(g,t0); v.gain.exponentialRampToValueAtTime(0.0001,t0+dur); s.connect(h).connect(l).connect(v).connect(a.destination); s.start(t0); s.stop(t0+dur+0.02);}catch(e){} } | |
| const SFX={ | |
| alarm(){ tone(880,.09,{g:.035}); tone(880,.09,{g:.035,at:.14}); tone(1320,.18,{g:.03,at:.28}); }, | |
| patrol(){ tone(523,.08,{type:'triangle',g:.04}); tone(659,.14,{type:'triangle',g:.04,at:.1}); }, | |
| voice(){ noise(.06,{g:.05,hp:1500,lp:6000}); tone(440,.1,{at:.08}); tone(660,.16,{at:.2}); for(let i=0;i<9;i++)tone(150+((i*37)%60),.07,{type:'sawtooth',g:.02,at:.45+i*.1,to:190}); }, | |
| strobe(){ for(let i=0;i<8;i++)tone(2200,.03,{g:.025,at:i*.16}); }, | |
| siren(){ tone(600,1.0,{type:'sawtooth',g:.03,to:1100}); tone(1100,1.0,{type:'sawtooth',g:.03,to:600,at:1.0}); tone(600,.6,{type:'sawtooth',g:.02,to:1000,at:2.0}); }, | |
| police(){ for(let i=0;i<6;i++)tone(i%2?900:700,.22,{g:.025,to:i%2?700:900,at:i*.22}); }, | |
| guard(){ noise(.12,{g:.04,hp:600,lp:2500}); tone(1000,.05,{g:.03,at:.14}); noise(.08,{g:.03,hp:600,lp:2500,at:.5}); }, | |
| good(){ tone(523,.08,{g:.03}); tone(659,.08,{g:.03,at:.08}); tone(784,.14,{g:.03,at:.16}); }, | |
| bad(){ tone(220,.28,{g:.035,to:120}); }, | |
| clear(){ tone(660,.06,{type:'triangle',g:.04}); tone(880,.12,{type:'triangle',g:.04,at:.07}); }, | |
| finish(){ [523,659,784,1047].forEach((f,i)=>tone(f,.16,{type:'triangle',g:.04,at:i*.13})); tone(1047,.5,{type:'triangle',g:.03,at:.55}); }, | |
| }; | |
| const muteBtn=document.getElementById('mute'); | |
| try{ muted=sessionStorage.getItem('vg_muted')==='1'; }catch(e){} | |
| function paintMute(){ muteBtn.textContent=muted?'SOUND OFF':'SOUND ON'; muteBtn.setAttribute('aria-pressed',muted); } | |
| muteBtn.onclick=()=>{ muted=!muted; paintMute(); try{sessionStorage.setItem('vg_muted',muted?'1':'0');}catch(e){} if(muted){ silentEl.pause(); } else { unlocked=false; unlock(); SFX.good(); } }; paintMute(); | |
| /* ============ Best scores ============ */ | |
| function bestGet(id){ try{ return JSON.parse(localStorage.getItem('vg_best')||'{}')[id]; }catch(e){ return undefined; } } | |
| function bestSet(id,v){ try{ const b=JSON.parse(localStorage.getItem('vg_best')||'{}'); if(b[id]===undefined||v>b[id]){ b[id]=v; localStorage.setItem('vg_best',JSON.stringify(b)); return true; } }catch(e){} return false; } | |
| /* ============ Flow ============ */ | |
| const $=id=>document.getElementById(id); | |
| function show(id){ for(const p of ['menu','play','result'])$(p).classList.toggle('hidden',p!==id); postHeight(); } | |
| function hudScore(){ $('hudScore').textContent=current?('SCORE '+score):''; } | |
| function buildMenu(){ | |
| const wrap=$('sites'); wrap.innerHTML=''; | |
| SCENARIOS.forEach(s=>{ | |
| const b=document.createElement('button'); b.className='site'; | |
| const c=document.createElement('canvas'); c.width=320;c.height=180; b.appendChild(c); | |
| const best=bestGet(s.id); | |
| b.insertAdjacentHTML('beforeend',`<span>${s.name}</span><small>${s.sub}</small>${best!==undefined?`<em>BEST ${best}/${s.max}</em>`:''}`); | |
| b.onclick=()=>start(s); wrap.appendChild(b); | |
| const tctx=c.getContext('2d'); tctx.imageSmoothingEnabled=false; const main=ctx; ctx=tctx; SCENES[s.id]({actors:[],cam:s.cam,...(s.steps[0].scene)}); siteLabel(s.name,s.sub,true); ctx=main; | |
| }); | |
| sceneOverride={kind:'dealer',actors:[],cam:'CAM 01 FRONT ROW'}; | |
| } | |
| function start(s){ current=s; stepIdx=0; score=0; recap=[]; sceneOverride=null; fx={type:null,t0:0}; lastPick=null; show('play'); renderStep(); hudScore(); window.scrollTo({top:0,behavior:'smooth'}); } | |
| function renderStep(){ | |
| const st=current.steps[stepIdx]; | |
| $('stepNum').textContent=(stepIdx+1)+'/'+current.steps.length; $('stepTitle').textContent=st.title; | |
| const tag=$('stepTag'); tag.textContent=st.kind==='patrol'?'PATROL':'ALARM'; tag.className='tag '+st.kind; | |
| $('dots').innerHTML=current.steps.map((_,i)=>`<i class="${i<stepIdx?'on':(i===stepIdx?'now':'')}"></i>`).join(''); | |
| $('narr').innerHTML=st.narr; $('hudCam').textContent=st.cam; | |
| const wrap=$('actions'); wrap.innerHTML=''; | |
| ACTIONS.forEach((a,i)=>{ const b=document.createElement('button'); b.className='act '+a.cls; b.innerHTML=`<span class="k">${i+1}</span>${a.label}`; b.onclick=()=>choose(a.id); wrap.appendChild(b); }); | |
| $('fb').classList.add('hidden'); (st.kind==='patrol'?SFX.patrol:SFX.alarm)(); postHeight(); | |
| } | |
| function enableActs(){ document.querySelectorAll('.act').forEach(b=>b.disabled=false); } | |
| function choose(id){ | |
| if(document.querySelector('.act[disabled]'))return; | |
| const st=current.steps[stepIdx]; | |
| const o=st.opts[id]||{...D[id],go:'stay'}; | |
| score+=o.pts; hudScore(); | |
| if(o.fx){ fx={type:o.fx,t0:performance.now()}; SFX[o.fx](); } else if(id==='clear'&&o.pts>0){ SFX.clear(); } else { (o.pts>0?SFX.good:SFX.bad)(); } | |
| $('fb').className='feedback '+(o.pts>=10?'':(o.pts>=0?'ok':'bad')); | |
| $('fbPts').textContent=(o.pts>0?'+':'')+o.pts+' points'; $('fbText').textContent=o.txt; $('fb').classList.remove('hidden'); | |
| const label=ACTIONS.find(a=>a.id===id).label; | |
| lastPick={pts:o.pts,recapAdded:o.go!=='stay'}; | |
| if(o.go!=='stay')recap.push({step:st.title,label,pts:o.pts}); | |
| $('nextBtn').textContent=o.go==='end'?'See results':(o.go==='stay'?'Try again':'Continue'); | |
| $('redoBtn').classList.toggle('hidden',o.go==='stay'); | |
| $('nextBtn').onclick=()=>{ fx={type:null,t0:0}; if(o.go==='end')finish(); else if(o.go==='next'){stepIdx++;renderStep();} else {$('fb').classList.add('hidden'); enableActs();} }; | |
| document.querySelectorAll('.act').forEach(b=>b.disabled=true); $('nextBtn').focus({preventScroll:true}); postHeight(); | |
| } | |
| $('redoBtn').onclick=()=>{ if(!lastPick)return; score-=lastPick.pts; if(lastPick.recapAdded)recap.pop(); lastPick=null; hudScore(); fx={type:null,t0:0}; $('fb').classList.add('hidden'); enableActs(); }; | |
| $('restart').onclick=()=>start(current); | |
| function finish(){ | |
| show('result'); SFX.finish(); $('rScore').textContent=score; $('rMax').textContent='of '+current.max; | |
| const pct=score/current.max; const newBest=bestSet(current.id,score); | |
| let title,note; | |
| if(pct>=0.9){ title='Command Center ready.'; note='You verified before acting, warned before escalating, and gave officers what they needed. That is the job.'; } | |
| else if(pct>=0.65){ title='Solid shift.'; note='The incident ended well. A few escalations came early or late. Verify first, warn second, escalate when they stay.'; } | |
| else if(pct>=0.35){ title='Trainee shift.'; note='The subject was handled, but the order of operations cost points. Most incidents end at the voice down when it comes at the right moment.'; } | |
| else { title='Back to the classroom.'; note='Early police calls and cleared events without verification are the two mistakes clients feel most. Watch first, then act.'; } | |
| $('rTitle').textContent=title+(newBest?' New best for this site.':''); $('rNote').textContent=note; | |
| $('recap').innerHTML=recap.map(r=>`<li><b>${r.step}</b>: ${r.label} (${r.pts>0?'+':''}${r.pts})</li>`).join(''); | |
| sceneOverride=null; | |
| } | |
| $('again').onclick=()=>{ current=null; hudScore(); $('hudCam').textContent='CAM --'; buildMenu(); show('menu'); }; | |
| $('replay').onclick=()=>start(current); | |
| document.addEventListener('keydown',e=>{ | |
| if($('play').classList.contains('hidden'))return; | |
| const n=parseInt(e.key,10); if(n>=1&&n<=7&&!document.querySelector('.act[disabled]')){ choose(ACTIONS[n-1].id); e.preventDefault(); } | |
| if(e.key==='Enter'&&!$('fb').classList.contains('hidden')&&document.activeElement!==$('nextBtn')){ $('nextBtn').click(); e.preventDefault(); } | |
| }); | |
| /* iframe auto height: the parent page can listen for {type:'vg-game-height', height} */ | |
| function postHeight(){ try{ if(window.parent!==window)requestAnimationFrame(()=>window.parent.postMessage({type:'vg-game-height',height:document.documentElement.scrollHeight},'*')); }catch(e){} } | |
| window.addEventListener('resize',postHeight); | |
| buildMenu(); requestAnimationFrame(draw); postHeight(); | |
| </script> | |
| </body> | |
| </html> | |