Skip to content

Latest commit

Β 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 

Repository files navigation

<!doctype html>

<title>L'ura Phase 4 β€” 3D</title> <style> :root{color-scheme:dark; --bg:#060812;--panel:rgb(12 14 24/88%);--text:#f5f7ff; --muted:#aeb5ca;--blue:#85d8ff;--gold:#ffd45a;--danger:#ff5f86;} *{box-sizing:border-box;} body{margin:0;overflow:hidden;background:var(--bg);color:var(--text); font-family:Inter,ui-sans-serif,system-ui,sans-serif;} #scene{position:fixed;inset:0;} .hud{position:fixed;left:16px;top:16px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;z-index:2;pointer-events:none;} .pill,.controls,.legend{background:var(--panel);border:1px solid rgb(255 255 255/13%);border-radius:8px;box-shadow:0 18px 50px rgb(0 0 0/30%);} .pill{padding:8px 10px;color:var(--muted);font-size:13px;} .pill strong{color:var(--text);} .controls{position:fixed;right:16px;top:16px;width:252px;padding:14px;z-index:3;} .controls h1{margin:0 0 4px;font-size:18px;} .controls p{margin:0 0 10px;color:var(--muted);font-size:12px;line-height:1.35;} .row{display:flex;gap:8px;align-items:center;margin-top:10px;} button{border:1px solid rgb(255 255 255/14%);border-radius:8px;background:#1d2134; color:var(--text);padding:8px 10px;font:inherit;cursor:pointer;} button:hover{border-color:rgb(255 255 255/30%);} input[type="range"]{width:100%;accent-color:var(--blue);} .helper-row{margin-top:10px;display:flex;align-items:flex-start;gap:8px;} .helper-row input[type="checkbox"]{margin-top:2px;accent-color:var(--gold);width:15px;height:15px;flex-shrink:0;cursor:pointer;} .helper-row label{font-size:11px;color:var(--muted);cursor:pointer;line-height:1.4;} .helper-row label strong{color:#ffcc44;} .legend{position:fixed;left:16px;bottom:16px;width:min(540px,calc(100vw - 32px)); padding:12px 14px;color:var(--muted);font-size:12px;line-height:1.5;z-index:2;} .legend strong{color:var(--text);}
#raidcall{position:fixed;top:68px;left:50%;transform:translateX(-50%);
  display:flex;gap:6px;align-items:stretch;
  background:rgb(4 6 16/86%);border:1px solid rgb(255 255 255/12%);
  border-radius:10px;padding:8px 12px;z-index:4;pointer-events:none;
  opacity:0;transition:opacity .25s;}
#raidcall.show{opacity:1;}
.sp-slot{display:flex;flex-direction:column;align-items:center;gap:2px;
  padding:6px 14px;border-radius:6px;border:1px solid transparent;min-width:78px;transition:all .15s;}
.sp-slot .dir{font-size:17px;font-weight:900;color:var(--muted);white-space:nowrap;}
.sp-slot .lbl{font-size:11px;color:var(--muted);}
.sp-slot.stepping .dir,.sp-slot.stepping .lbl{color:var(--gold);}
.sp-slot.stepping{border-color:var(--gold);background:rgb(255 212 90/12%);}

#movecall{position:fixed;top:44%;left:50%;transform:translate(-50%,-50%);
  text-align:center;z-index:4;pointer-events:none;opacity:0;transition:opacity .18s;}
#movecall.show{opacity:1;}
#movecall .movelabel{font-size:20px;font-weight:800;color:var(--gold);
  text-shadow:0 2px 14px rgba(0,0,0,.95);letter-spacing:.12em;}
#movecall .bignum{font-size:104px;font-weight:900;line-height:.95;color:#fff;
  text-shadow:0 0 48px rgba(255,212,90,.8),0 4px 24px rgba(0,0,0,.95);}

#message{position:fixed;top:20%;left:50%;transform:translateX(-50%);
  font-size:30px;font-weight:900;text-align:center;z-index:5;pointer-events:none;
  text-shadow:0 2px 20px rgba(0,0,0,.95);opacity:0;transition:opacity .25s;
  letter-spacing:.04em;white-space:nowrap;}
#message.show{opacity:1;}
#message.danger{color:var(--danger);}
#message.bad{color:#ff9f1c;}
#message.alert{color:#fff;text-shadow:0 0 32px rgba(255,212,90,.9),0 2px 20px rgba(0,0,0,.95);}

/* Mode select overlay */
#mode-select{position:fixed;inset:0;z-index:200;
  background:rgb(6 8 18/97%);backdrop-filter:blur(4px);
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;}
#mode-select h2{font-size:26px;font-weight:900;color:#fff;margin:0;text-align:center;letter-spacing:.06em;}
#mode-select .mode-sub{color:var(--muted);font-size:13px;margin:0;text-align:center;}
.mode-btn{width:280px;padding:14px 22px;border-radius:10px;font-size:14px;font-weight:700;
  border:2px solid;cursor:pointer;text-align:left;transition:all .15s;background:transparent;font-family:inherit;}
.mode-btn .mode-title{display:block;font-size:15px;}
.mode-btn .mode-desc{display:block;font-size:11px;font-weight:400;margin-top:3px;opacity:.75;}
.mode-easy{border-color:#a8e563;color:#a8e563;}
.mode-easy:hover{background:rgb(168 229 99/14%);}
.mode-normal{border-color:#85d8ff;color:#85d8ff;}
.mode-normal:hover{background:rgb(133 216 255/14%);}
.mode-chaotic{border-color:#ff9f1c;color:#ff9f1c;}
.mode-chaotic:hover{background:rgb(255 159 28/14%);}
/* Role selector */
#role-step,#diff-step{display:flex;flex-direction:column;align-items:center;gap:14px;}
.role-btn{width:280px;padding:12px 20px;border-radius:10px;font-size:13px;font-weight:700;
  border:2px solid;cursor:pointer;text-align:left;transition:all .15s;background:transparent;font-family:inherit;}
.role-btn .role-title{display:block;font-size:14px;}
.role-btn .role-desc{display:block;font-size:11px;font-weight:400;margin-top:3px;opacity:.75;}
.role-dps{border-color:#85d8ff;color:#85d8ff;}.role-dps:hover{background:rgb(133 216 255/14%);}
.role-light{border-color:#ffe066;color:#ffe066;}.role-light:hover{background:rgb(255 224 102/14%);}
.role-tank{border-color:#ff7a45;color:#ff7a45;}.role-tank:hover{background:rgb(255 122 69/14%);}
.step-back{background:none;border:none;color:var(--muted);font-size:12px;cursor:pointer;
  font-family:inherit;padding:4px 8px;text-decoration:underline;margin-top:-4px;}
.step-back:hover{color:#fff;}
.eab-row{display:flex;align-items:center;gap:8px;}
.eab-row label{font-size:12px;color:var(--muted);}
.eab-key-btn{width:38px;height:38px;border-radius:6px;border:2px solid #ff7a45;
  background:rgb(255 122 69/12%);color:#ff7a45;font-size:13px;font-weight:700;
  cursor:pointer;font-family:inherit;}
.eab-key-btn.capturing{border-color:#fff;color:#fff;animation:blink .5s infinite;}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.35}}
/* EAB cooldown icon */
#eab-icon{position:fixed;bottom:88px;left:50%;transform:translateX(-50%);
  z-index:5;display:none;flex-direction:column;align-items:center;gap:4px;}
#eab-icon.show{display:flex;}
#eab-icon canvas{border-radius:8px;border:2px solid #ff7a45;display:block;}
#eab-label{font-size:10px;color:#ff7a45;font-weight:700;letter-spacing:.08em;}
/* Defensive charges */
#def-charges{position:fixed;bottom:88px;left:calc(50% + 64px);
  z-index:5;display:none;flex-direction:column;align-items:center;gap:4px;}
#def-charges.show{display:flex;}
#def-pips{display:flex;gap:4px;}
.def-pip{width:14px;height:14px;border-radius:3px;border:2px solid #66aaff;background:rgb(102 170 255/35%);}
.def-pip.spent{border-color:#333;background:transparent;}
#def-key-label{font-size:10px;color:#66aaff;font-weight:700;letter-spacing:.08em;}
/* Defensive bar */
#def-bar-wrap{position:fixed;top:25%;left:50%;transform:translateX(-50%);
  width:320px;z-index:5;display:none;flex-direction:column;align-items:center;gap:4px;}
#def-bar-wrap.show{display:flex;}
.def-bar-label{font-size:12px;font-weight:700;color:#66aaff;letter-spacing:.1em;text-shadow:0 0 12px rgba(100,180,255,.6);}
.def-track{width:100%;height:14px;background:rgb(0 0 0/55%);border:1px solid rgb(100 170 255/35%);border-radius:4px;overflow:hidden;}
#def-fill{height:100%;width:100%;background:linear-gradient(90deg,#3399ff,#88ccff);border-radius:4px;transition:width .1s linear;}
/* Penalty flash */
#penalty-flash{position:fixed;inset:0;z-index:3;pointer-events:none;
  background:rgba(220,0,0,0.4);opacity:0;transition:opacity .22s ease-out;}
#penalty-flash.on{opacity:1;transition:none;}
/* Starsplinter flash β€” blue edge vignette */
#sp-flash{position:fixed;inset:0;z-index:3;pointer-events:none;
  box-shadow:inset 0 0 110px 30px rgba(60,170,255,0.7);opacity:0;transition:opacity .55s ease-out;}
#sp-flash.on{opacity:1;transition:none;}
/* Reintegration hint */
#reint-hint{position:fixed;top:52%;left:50%;transform:translate(-50%,-50%);
  font-size:15px;font-weight:600;color:#85d8ff;text-align:center;z-index:4;pointer-events:none;
  background:rgb(4 8 24/85%);border:1px solid rgb(133 216 255/28%);border-radius:8px;
  padding:11px 22px;opacity:0;transition:opacity .4s;line-height:1.6;max-width:420px;}
#reint-hint.show{opacity:1;}
/* Stun-phase role hints */
#stun-hints{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
  z-index:12;pointer-events:none;display:flex;flex-direction:column;
  align-items:center;gap:14px;width:min(680px,90vw);}
.hint-line{font-size:19px;font-weight:700;color:#fff;text-align:center;
  text-shadow:0 2px 18px rgba(0,0,0,.95),0 0 40px rgba(100,180,255,.2);
  background:rgb(4 8 24/82%);border:1px solid rgb(133 216 255/22%);
  border-radius:9px;padding:10px 22px;opacity:0;line-height:1.35;
  width:100%;box-sizing:border-box;}
/* Chaotic toggle */
.chaotic-row{margin-top:10px;padding-top:10px;border-top:1px solid rgb(255 255 255/10%);display:flex;align-items:flex-start;gap:8px;}
.chaotic-row input[type="checkbox"]{margin-top:2px;accent-color:#ff9f1c;width:15px;height:15px;flex-shrink:0;cursor:pointer;}
.chaotic-row label{font-size:11px;cursor:pointer;line-height:1.4;color:#ff9f1c;}
.chaotic-row label span{color:var(--muted);}
/* Boss cast bar */
#boss-castbar{position:fixed;top:10px;left:50%;transform:translateX(-50%);
  width:230px;z-index:4;pointer-events:none;opacity:0;transition:opacity .18s;
  background:rgb(0 0 0/78%);border:1px solid rgb(255 255 255/22%);border-radius:5px;
  padding:5px 8px;}
#boss-castbar.show{opacity:1;}
.bcb-label{font-size:11px;font-weight:700;color:#fff;margin-bottom:4px;text-align:center;
  letter-spacing:.06em;text-transform:uppercase;}
.bcb-track{height:11px;background:rgb(0 0 0/55%);border:1px solid rgb(255 255 255/14%);border-radius:3px;overflow:hidden;}
.bcb-fill{height:100%;width:0%;border-radius:3px;transition:background .2s;}
.bcb-fill.casting{background:linear-gradient(90deg,#7c3aed,#c084fc);}
.bcb-fill.channeling{background:linear-gradient(90deg,#ff2a43,#ff8c00);}
/* World Cup popup */
#worldcup{position:fixed;z-index:20;pointer-events:none;opacity:0;transition:opacity .15s;
  font-size:18px;font-weight:900;color:#fff;text-shadow:0 0 18px rgba(255,165,0,.9),0 2px 12px rgba(0,0,0,.9);
  background:rgb(10 20 5/88%);border:2px solid #ffcc00;border-radius:8px;padding:8px 14px;
  max-width:220px;line-height:1.3;}
#worldcup.show{opacity:1;}
/* Guild chat */
#guildchat{position:fixed;left:16px;bottom:130px;width:270px;z-index:6;pointer-events:none;
  background:rgb(4 6 14/82%);border:1px solid rgb(255 255 255/14%);border-radius:6px;
  opacity:0;transition:opacity .3s;max-height:200px;display:flex;flex-direction:column;overflow:hidden;}
#guildchat.show{opacity:1;}
.gc-header{font-size:10px;font-weight:700;padding:4px 8px;color:#aaa;border-bottom:1px solid rgb(255 255 255/10%);letter-spacing:.1em;}
#gc-messages{padding:4px 8px 6px;font-size:11px;line-height:1.65;overflow:hidden;}
.gc-msg{animation:gcfade .25s ease-in;}
@keyframes gcfade{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
.gc-name{color:#ffd45a;font-weight:700;}
.gc-leave{color:#8888aa;}
.gc-resp{color:#4dcc4d;}
/* Discord notification */
#discord-notif{position:fixed;right:16px;bottom:80px;width:270px;z-index:6;pointer-events:none;
  background:rgb(28 29 33/96%);border:1px solid rgb(255 255 255/12%);border-radius:8px;
  padding:10px 12px;display:flex;gap:10px;align-items:flex-start;
  opacity:0;transition:opacity .2s;box-shadow:0 6px 24px rgb(0 0 0/50%);}
#discord-notif.show{opacity:1;}
.dc-app{font-size:10px;color:#72767d;margin-bottom:2px;letter-spacing:.06em;}
.dc-sender{font-size:12px;font-weight:700;color:#00aff4;margin-bottom:2px;}
.dc-text{font-size:12px;color:#dcddde;line-height:1.4;}
.dc-icon{font-size:22px;flex-shrink:0;margin-top:2px;}
/* LUA error dialog */
#lua-error{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
  z-index:60;width:480px;max-width:calc(100vw - 32px);
  background:#1a1208;border:2px solid #c8a020;border-radius:3px;
  box-shadow:0 0 60px rgba(0,0,0,.98),0 0 0 1px #4a3800;
  font-family:'Courier New',monospace;display:none;}
#lua-error.show{display:block;}
.lua-title{background:#3d2800;padding:6px 12px;font-size:13px;font-weight:700;
  color:#ffd45a;border-bottom:1px solid #6a4800;letter-spacing:.06em;}
.lua-body{padding:12px;max-height:190px;overflow-y:auto;
  font-size:11px;color:#dd9900;line-height:1.65;word-break:break-all;white-space:pre-wrap;}
.lua-footer{padding:8px 12px;border-top:1px solid #3a2800;display:flex;justify-content:flex-end;}
.lua-close-btn{background:#3d2800;border:1px solid #c8a020;color:#ffd45a;
  padding:5px 20px;font:inherit;font-size:12px;cursor:pointer;border-radius:2px;}
.lua-close-btn:hover{background:#5a3c00;}
/* Death reaction */
#death-msg{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
  font-size:38px;font-weight:900;text-align:center;z-index:5;pointer-events:none;
  text-shadow:0 0 32px rgba(255,30,30,.95),0 3px 20px rgba(0,0,0,.95);
  color:#ff5f86;opacity:0;transition:opacity .18s;white-space:nowrap;letter-spacing:.03em;}
#death-msg.show{opacity:1;}
/* Cast bar */
#castbar{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);
  width:340px;z-index:4;pointer-events:none;opacity:0;transition:opacity .2s;}
#castbar.show{opacity:1;}
#castbar .cast-label{font-size:13px;font-weight:600;color:var(--gold);margin-bottom:4px;text-align:center;
  text-shadow:0 1px 8px rgba(0,0,0,.9);letter-spacing:.06em;}
#castbar .cast-track{height:18px;background:rgb(0 0 0/55%);border:1px solid rgb(255 255 255/18%);
  border-radius:4px;overflow:hidden;}
#castbar .cast-fill{height:100%;background:linear-gradient(90deg,#ffd45a,#fff7a0);
  width:0%;transition:width .06s linear;border-radius:4px;}

/* Scoreboard */
#scoreboard{position:fixed;inset:0;z-index:100;display:none;
  background:rgb(0 0 0/75%);backdrop-filter:blur(6px);
  align-items:center;justify-content:center;}
#scoreboard.show{display:flex;}
.score-card{background:#0d1022;border:1px solid rgb(255 255 255/18%);
  border-radius:16px;padding:32px 40px;min-width:380px;max-width:480px;
  box-shadow:0 32px 80px rgb(0 0 0/60%);}
.score-card h2{margin:0 0 4px;font-size:22px;text-align:center;color:var(--muted);letter-spacing:.12em;text-transform:uppercase;}
.score-grade{font-size:88px;font-weight:900;text-align:center;line-height:1;margin:4px 0;}
.score-grade.gAp{color:#ffd45a;text-shadow:0 0 50px rgba(255,212,90,.9);}
.score-grade.gA {color:#a8e563;}
.score-grade.gB {color:#85d8ff;}
.score-grade.gC {color:#fff;}
.score-grade.gD {color:#ff9f1c;}
.score-grade.gF {color:#ff5f86;}
.score-grade.gFm{color:#ff5f86;text-shadow:0 0 30px rgba(255,30,30,.6);}
.score-pts{text-align:center;font-size:14px;color:var(--muted);margin-bottom:16px;}
.score-pts strong{color:var(--text);font-size:18px;}
.score-breakdown{border-top:1px solid rgb(255 255 255/10%);padding-top:14px;margin-bottom:16px;}
.score-row{display:flex;justify-content:space-between;font-size:12px;color:var(--muted);padding:2px 0;}
.score-row .val{font-weight:700;}
.score-row .val.pos{color:#a8e563;}
.score-row .val.neg{color:var(--danger);}
.score-actions{display:flex;gap:10px;justify-content:center;}
.score-actions button{padding:10px 24px;font-size:14px;}
</style>

L'ura β€” Phase 4

Choose your role

β–Ά DPS / Healer Standard play Β· Assigned Starsplinters πŸ”† Light Holder Guide the raid with your light zone Β· 3 AI Starsplinters πŸ›‘ Add Tank Soak adds Β· Control the cone Β· 3 AI Starsplinters
Extra Action: E Defensive: F
Choose Difficulty Proceed to difficulty selection

L'ura β€” Phase 4

DPS / Healer Β· Choose difficulty

β–Ά Play Easy Movement helper enabled Β· 0.5Γ— score β–Ά Play Normal No helper Β· Full score πŸ”₯ Chaotic Phase 4 Attempt Distractions Β· Raid dies Β· 1.15Γ— score ← Change role
EAB
F
DEFENSIVE UP
During re-integration, move to the
stack point at the north of the room.
Time 0.0s
State Reintegration
Cycle 1 / 4
Score 0
β¬… LEFT
Starsplinter 1
RIGHT ➑
Starsplinter 2
β¬… LEFT
Starsplinter 3
MOVE IN
3
REINTEGRATION

Phase Complete

NORMAL
A+
Score: 0
Time safe in light circle+0
Players sliced (βˆ’10 each)βˆ’0
Got sliced (βˆ’10 each)βˆ’0
Got munched (βˆ’5 each)βˆ’0
Midnight Falls (βˆ’100 each)βˆ’0
Got zapped (βˆ’100 each)βˆ’0
Wrong side on Starsplinter (βˆ’15 each)βˆ’0
Soaked without defensive (βˆ’5 each)βˆ’0
Adds hit raid β€” no EAB (βˆ’75 each)βˆ’0
Play Again

L'ura P4 3D

ZQSD β€” Z/S toward/away boss, Q/D strafe. White cylinder = you. Starsplinter assigned randomly each cycle.

Pause Restart
Speed
Camera distance
Camera height
Show movement helper (Starsplinter target ring)
⚠ Enabled: score Γ—0.5 per second
Show position markers
Coloured cylinders at raid spots
πŸ”₯ CHAOTIC FINAL PHASE ATTEMPT
Surprise callouts · Raid dies · 1.3pts/s · ⚽
Heaven and Hell
βš” GUILD CHAT
⚠ Interface\AddOns Error
Close
πŸ’¬
Discord β€” Direct Message
Josh
Hey, you coming over later?
White: you Β |Β  Blue: raid Β |Β  Gold: OT + cone Β |Β  Yellow disc: light safe zone Β |Β  Cyan ring: your Starsplinter target Β |Β  Orientation A/B rotates shard pattern 45Β°
<script type="module"> import * as THREE from "https://unpkg.com/three@0.165.0/build/three.module.js"; // ── DOM ────────────────────────────────────────────────────────────────── const mount=document.getElementById("scene"); const clockEl=document.getElementById("clock"),stateEl=document.getElementById("state"),cycleEl=document.getElementById("cycle"); const scorePillEl=document.getElementById("scorepill"); const playBtn=document.getElementById("play"),restartBtn=document.getElementById("restart"),speedInput=document.getElementById("speed"); const camDistInput=document.getElementById("camDist"),camHeightInput=document.getElementById("camHeight"); const spFlashEl=document.getElementById("sp-flash"); const helperCheck=document.getElementById("helper"),chaoticCheck=document.getElementById("chaotic"),markersCheck=document.getElementById("markers"); const worldCupEl=document.getElementById("worldcup"); const guildChatEl=document.getElementById("guildchat"),gcMsgsEl=document.getElementById("gc-messages"); const discordEl=document.getElementById("discord-notif"),dcSenderEl=document.getElementById("dc-sender"),dcTextEl=document.getElementById("dc-text"); const raidcallEl=document.getElementById("raidcall"),movecallEl=document.getElementById("movecall"); const movecountdownEl=document.getElementById("movecountdown"),messageEl=document.getElementById("message"); const spSlotEls=[0,1,2].map(i=>document.getElementById(`sp${i}`)); const castbarEl=document.getElementById("castbar"),castfillEl=document.getElementById("castfill"); const bossCastbarEl=document.getElementById("boss-castbar"),bcbLabelEl=document.getElementById("bcb-label"),bcbFillEl=document.getElementById("bcb-fill"); const scoreboardEl=document.getElementById("scoreboard"); const luaErrorEl=document.getElementById("lua-error"),luaErrorBodyEl=document.getElementById("lua-error-body"); const deathMsgEl=document.getElementById("death-msg"); const reintHintEl=document.getElementById("reint-hint"); const penaltyFlashEl=document.getElementById("penalty-flash"); let prevScore=0,penaltyFlashExpiry=-999,spFlashExpiry=-999; const modeSelectEl=document.getElementById("mode-select"); document.getElementById("lua-close-btn").addEventListener("click",()=>{luaErrorEl.classList.remove("show");luaErrorShowing=false;}); // ── Phase / world ──────────────────────────────────────────────────────── const phase={ duration:90,reintegrationCast:5, firstStarsplinter:16,starsplinterSpacing:20, heavenSpacing:20,firstHeaven:22, starsApplyGap:1,starsExplosionDelay:2, moveOutDuration:1.0,returnDuration:1.45, stunDuration:7,heavenRampDuration:4,heavenLaserDuration:4, raidDirection:-1,raidAdvanceDegrees:110, shaBaseInterval:0.4,shaSpeed:28,shaWaveSpeed:38, tankConeDelay:0.5,tankConeDuration:0.5 }; const world={ roomRadius:285,bossRadius:36,stackDistance:134, offTankDistance:126,lightRadius:60,shardLength:90,coneLength:109 }; const scale=0.052; const splinterSides=[1,-1,1]; const splinterProfiles=[{tangential:46,radial:13},{tangential:58,radial:14},{tangential:56,radial:10}]; const immunityColors=[0xaad372,0xffd45a,0xff89c2]; const MARKER_COLORS=[0xffdd00,0xff8c00,0xaa44ff,0x44ff88]; const CAM_BACK=42,CAM_H=17.5; // ── State ──────────────────────────────────────────────────────────────── let time=0,last=performance.now(),running=false; let playerSplinterForceExplodeAt=Infinity; let selectedMode='normal'; let selectedRole='dps'; // 'dps' | 'light' | 'tank' let eabKey='e',eabCooldownUntil=-Infinity,eabConePos=null,eabConeDir={x:0,y:1},eabConeExpiry=-Infinity,eabConeFired=false; const tankEabWaveKillCycles=new Set(),checkedWavePenalties=new Set(); let defKey='f',defCharges=3,defExpiry=-Infinity; let lightMovePenaltyExpiry=-Infinity; let seed=5,lastAddTime=-999,nextAddId=1; let adds=[],dyingAdds=[],waveSpawned=new Set(); let playerPos={x:0,y:-world.stackDistance}; let playerFacingDir={x:0,y:1}; // fixed facing: toward boss; only updates during movement phases const keys={}; const cycleAssignments={},cycleOrientations={},cycleDelays={},cycleAdjustedDrops={},cycleFakeouts={},cycleSafeDrops={}; let nextWanderTime=20+Math.random()*12; const checkedExplosions=new Set(),checkedLasers=new Set(),firedConeKills=new Set(); let messageExpiry=0; let lastMunchTime=-999,lastMidnightTime=-999; // Chaotic mode state const deadAI=new Set(); let worldCupExpiry=-999,nextWorldCupTime=18; const WCGOALS=['⚽ World Cup Goal Scored!!!!','⚽ GOOOOOAL!!!!','⚽ HE SHOOTS HE SCORES!!!!','⚽ UNBELIEVABLE SCENES!!!!']; const chaoticSPLines=['RIGHT NO LEFT NO I MEAN LEFT','LEFT?? NO WAIT RIGHT!! LEFT!!!','WHICH SIDE AGAIN?? GO!! NOW!!','LEFT I THINK?? JUST MOVE!!']; const GUILD_NAMES=['Ruby','Cydonion','Layelle','Boltern','Josh','Darktheist','Dizzypete','Catters','Max','Sessanelly','Slowbolts','Aratiel','Zafu','Convoke','Exality','Tarrackk','Unfortunate','Stigdk','Girlstreamer','Fred','Klkl','Mox','Melwalor','Anaxstrasza','Sekrai','Zivvs','Jaffa','Desolate','Khiraen','Nordisk','Denarr','Tuulip','Luca','Silccu','Mikhe','Rquin','Kalse','Sproodle','Shadowdager','Itrinx','Yaela','Fester']; const GUILD_FIXED={'Sekrai':'bad joke','Jaffa':'thats cooked actually','Fester':'πŸ‡«πŸ‡·'}; function guildResp(name,fallback){return GUILD_FIXED[name]||fallback;}; const GUILD_RESPONSES=['omg','wtf','Good Riddance','wait what','nooo','the audacity','???','lmao','finally','I knew it','lol bye','carry diff','thank god','bruh','oof','not surprised','cya','drama queen','toxic tbh','expected','L + ratio','real ones know','this is fine','who tanks now','I called it','rip bozo','no way','was it something I said','lmaooo','the plot thickens','and they were our best dps too','ngl saw this coming','big yikes','free at last','don\'t let the door hit you','been months coming tbh','does officer know','wait when did they even join','pour one out','who does their role now','kicked or left?','classic','press F','F','insert surprised pikachu','honestly deserved','touched grass we assume','another one bites the dust','guild on life support at this point','RIP','we go again','bro really said bye mid progression']; const DISCORD_NAMES=['Josh','Layelle','Mum','Sarah','Dave','Conor','Alex','Catters','Boltern']; const DISCORD_MSGS=["Hey, you coming over later?","You left the fridge open","I'm thinking of quitting the game","Did you see the game last night?","Dinner's ready","You still owe me Β£20","Can you walk the dog?","Just found your cat on my roof","Meeting at 9am tomorrow","We need to talk","Your package arrived","Are you even listening?","I found your wallet","Stop ignoring me lol","Bro your car is blocking mine","Did you remember to take your meds?","Your parking ticket expires in 10 minutes","I think I scratched your car","The cat knocked everything off the shelf again","Can you pick up milk on the way home?","Why is there a horse in the front garden","Did you change the WiFi password??","Bro wake up new patch dropped","Your sister called again","I accidentally sent that to the group chat","The dishwasher is making that noise again","Did you pay the electricity bill","Your boss called the house phone","I think I failed my exam","The pizza arrived but I ate some of it","Why is there a tent in the living room","Someone keyed your car mate","I've been accepted into a cult, brb","Bro log off it's 3am","Your mum added me on Facebook","I'm outside your front door","Sorry wrong number","Did you feed the fish","There's a spider and I need you home NOW","I accidentally bought a boat","You've been selected for jury duty","I told everyone about that thing","Are you coming to Karen's wedding or not","My router keeps dropping, can I come over","Just remembered I still have your charger from 2019"]; const EASTER_EGGS=[ {name:'Darktheist',text:'it just keeps growing back, I gotta keep cuttin'}, {name:'Darktheist',text:"i'm not in it, I'm not in it bro!!! I'm not even in it!!!"}, {name:'Layelle',text:'use cooldowns anyone you have'}, {name:'Layelle',text:'I have 18 atonements rn'}, {name:'Jamesmarcus',text:"Why can't you soak?!?!"}, {name:'Josh',text:"I'm thinking of re-rolling for the 246th time this tier, shall I play monk?"}, {name:'Layelle',text:'turn on in game music this is SO good'}, {name:'Elizaslein',text:'can we take a break soon btw?'}, {name:'Zafu',text:"I'm thinking of pinging the entire raid team for the 15th time this evening. @LHF @LHF @LHF"}, {name:'Max',text:"I'm not the max you're thinking of."}, {name:'Ruby',text:'SOLO SOAKING'}, {name:'Ruby',text:'I am English'}, {name:'Ruby',text:'My class does no dps just none at all what the -',reply:{name:'Zafu',text:'Shut up.'}} ]; let easterEggSchedule=[],gcEggExpiry=-999,pendingGcReplies=[]; function buildEasterEggSchedule(){ easterEggSchedule=[]; const count=1+Math.floor(Math.random()*2); const picks=[...EASTER_EGGS].sort(()=>Math.random()-.5).slice(0,count); const base=[15+Math.random()*25,50+Math.random()*25]; picks.forEach((egg,i)=>easterEggSchedule.push({t:base[i],egg,fired:false}));} function addGuildChatMsg(name,text,type='resp'){ const d=document.createElement('div');d.className='gc-msg'; d.innerHTML=`${name}: ${text}`; gcMsgsEl.appendChild(d);} const CHAOS_MOVE_CALLS=['MOVE!!','WAIT!!','NO WAIT!!','GO!!','STOP!!','ACTUALLY MOVE!!']; const DEATH_REACTIONS=["I wasn't even in it!","DESK SLAM","What are you doing?!?!?!","bro WHAT","ARE YOU KIDDING ME??","MY KEYBOARD","I HATE THIS GAME","hello???? wtf","unbelievable","that's a rage DC","NOT MY FAULT","I WAS MOVING","LAG","REPORTED","are you serious rn","TOUCH GRASS","I can't believe this","my poor chair"]; const LUA_ERRORS=[ `Message: Interface\\FrameXML\\UIParent.lua:3092: attempt to index nil value 'self'\nstack traceback:\n[C]: ?\nInterface\\FrameXML\\UIParent.lua:3092: in function 'SetPoint'\nInterface\\AddOns\\RaidFrameHelper\\core.lua:887: in local 'UpdateRaidFrame'\nInterface\\AddOns\\RaidFrameHelper\\core.lua:1204: in function <...>\n...\n(tail call): ?`, `Message: Interface\\AddOns\\DBM-Core\\DBM-Core.lua:7341: attempt to perform arithmetic on nil value (global 'timerOffset')\nstack traceback:\n[C]: ?\nInterface\\AddOns\\DBM-Core\\DBM-Core.lua:7341: in method 'Start'\nInterface\\AddOns\\DBM-Raids-DF\\LuraP4.lua:448: in function 'COMBAT_LOG_EVENT_UNFILTERED'\n...\n(tail call): ?`, `Message: Interface\\AddOns\\WeakAuras\\RegionTypes\\Text.lua:512: table index is NaN\nstack traceback:\n[C]: ?\nInterface\\AddOns\\WeakAuras\\RegionTypes\\Text.lua:512: in function 'SetText'\nInterface\\AddOns\\WeakAuras\\WeakAuras.lua:2991: in function 'UpdateTriggerState'\n...\n(tail call): ?`, `Message: Interface\\FrameXML\\ActionButton.lua:234: Cannot find a library instance of 'LibStub-1.0'\nstack traceback:\n[C]: ?\nInterface\\AddOns\\ElvUI\\Core\\API.lua:234: in function 'Embed'\nInterface\\AddOns\\ElvUI\\Modules\\UnitFrames\\UF.lua:881: in function 'Initialize'\n...\n(tail call): ?`, `Message: Interface\\AddOns\\Plater\\Plater.lua:9042: attempt to call nil (method 'GetName')\nstack traceback:\n[C]: ?\nInterface\\AddOns\\Plater\\Plater.lua:9042: in method 'ApplyProfile'\nInterface\\AddOns\\Plater\\Plater.lua:1337: in function 'ADDON_LOADED'\n...\n(tail call): ?`, `Message: Interface\\AddOns\\Shadowed Unit Frames\\modules\\auras.lua:177: bad argument #1 to 'pairs' (table expected, got nil)\nstack traceback:\n[C]: in function 'pairs'\nInterface\\AddOns\\SUF\\modules\\auras.lua:177: in function 'UpdateAuras'\nInterface\\AddOns\\SUF\\units\\player.lua:44: in function 'FullUpdate'\n...\n(tail call): ?`]; const OFFLINE_RESPONSES=["during a pull?","ffs not again","every. single. time.","disconnect or rage?","classic","rip","pulled the plug lmao","not again bro","bruh","come on man","always at the worst time","we needed them"]; let chaosMoveText='WAIT!!',nextChaosMoveFlip=-999; let gcQueue=[],gcDramaStart=-999,gcDramaActive=false,nextGuildDrama=22; let nextOfflineTime=45; let dcExpiry=-999,nextDcTime=35; let luaErrorShowing=false,nextLuaErrorTime=28; let deathMsgExpiry=-999,lastDeathMsgTime=-999; const CHAOTIC_CORNERS=[ {top:'90px',left:'16px',right:'auto',bottom:'auto'}, {top:'90px',right:'270px',left:'auto',bottom:'auto'}, {bottom:'120px',left:'16px',top:'auto',right:'auto'}, {bottom:'120px',right:'270px',left:'auto',top:'auto'}]; // Score let score=0; const penalties={sliceOthers:0,sliced:0,munched:0,midnight:0,zapped:0,wrongSide:0}; let safeAccum=0; let scoreboardShown=false; window.addEventListener("keydown",e=>{ if("zqsd".includes(e.key.toLowerCase()))e.preventDefault(); keys[e.key.toLowerCase()]=true; if(selectedRole==='tank'&&running&&e.key.toLowerCase()===eabKey&&time>eabCooldownUntil &&time>phase.reintegrationCast+phase.stunDuration){ eabConePos={...playerPos};eabConeDir={...playerFacingDir}; eabConeExpiry=time+0.5;eabCooldownUntil=time+18;eabConeFired=false; const ac=activeStarsplinterCycle(time); if(ac&&waveSpawned.has(ac.index))tankEabWaveKillCycles.add(ac.index);} if(selectedRole==='tank'&&running&&e.key.toLowerCase()===defKey&&defCharges>0 &&time>phase.reintegrationCast+phase.stunDuration){ defCharges--;defExpiry=time+6; updateDefUI();}}); window.addEventListener("keyup",e=>{keys[e.key.toLowerCase()]=false;}); window.addEventListener("blur",()=>Object.keys(keys).forEach(k=>keys[k]=false)); function getCycleAssignment(i){if(selectedRole!=='dps')return -1;if(!(i in cycleAssignments))cycleAssignments[i]=Math.floor(Math.random()*3);return cycleAssignments[i];} function getCycleOrientation(i){if(!(i in cycleOrientations))cycleOrientations[i]=Math.random()<.5?1:0;return cycleOrientations[i];} const cyclePlayerOrientations={}; function getCyclePlayerOrientation(i){if(!(i in cyclePlayerOrientations))cyclePlayerOrientations[i]=Math.random()<.5?1:0;return cyclePlayerOrientations[i];} // SP2 bot sometimes hesitates ~1s before moving out (50% chance per cycle) function getCycleSP2Delay(i){if(!(i in cycleDelays))cycleDelays[i]=0.7+Math.random()*.6;return cycleDelays[i];} // Per-cycle fakeout: AI SP0/SP1 has 20% chance to move wrong way briefly then correct function getCycleFakeout(ci,drops){ if(ci in cycleFakeouts)return cycleFakeouts[ci]; const f={}; [0,1].forEach(si=>{ if(Math.random()<0.2){ f[si]={wrongDrop:drops[1-si],dur:0.4+Math.random()*.35};}}); cycleFakeouts[ci]=f;return f;} // Find a safe drop position where no shard ray hits the stack function findSafeDrop(defaultDrop,stackPos,orientB,safeR=18){ const check=pos=>{const angs=shardAnglesForDrop(stackPos,pos,orientB);for(const a of angs)if(shardHitsPos(pos,a,stackPos,safeR))return false;return true;}; if(check(defaultDrop))return defaultDrop; const rv=norm2(stackPos),tv={x:-rv.y,y:rv.x}; for(let off=8;off<=64;off+=8)for(const s of[1,-1]){const c=add2(defaultDrop,mul2(tv,off*s));if(check(c))return c;} return defaultDrop;} function getCycleSafeDrop(ci,si,stackPos,drops,orientB){ const key=`${ci}-${si}`; if(!(key in cycleSafeDrops))cycleSafeDrops[key]=findSafeDrop(drops[si],stackPos,orientB); return cycleSafeDrops[key];} // SP2 bot checks SP0's shards and picks a safe alternative drop once per cycle function getAdjustedSP2Drop(cycleIdx,stackPos,drops,orientB){ if(cycleIdx in cycleAdjustedDrops)return cycleAdjustedDrops[cycleIdx]; const sp0drop=drops[0],sp0angles=shardAnglesForDrop(stackPos,sp0drop,orientB); const hitR=18; function safe(pos){for(const ang of sp0angles){if(shardHitsPos(sp0drop,ang,pos,hitR))return false;}return true;} const maxR=world.lightRadius-4,r=norm2(stackPos),tan={x:-r.y,y:r.x}; function clampL(p){const rel=sub2(p,stackPos),d=len2(rel);return d>maxR?add2(stackPos,mul2(norm2(rel),maxR)):p;} const def=drops[2];let chosen=def; if(!safe(def)){ // Try radial/tangential shifts of increasing magnitude; commit to first safe spot const shifts=[[-15,0],[15,0],[0,12],[0,-12],[-15,12],[15,12],[-20,0],[0,20],[-15,-12],[15,-12]]; for(const[dr,dt]of shifts){ const c=clampL(add2(def,add2(mul2(r,dr),mul2(tan,dt)))); if(safe(c)){chosen=c;break;}}} cycleAdjustedDrops[cycleIdx]=chosen;return chosen;} // ── Three.js ───────────────────────────────────────────────────────────── const scene=new THREE.Scene(); scene.background=new THREE.Color(0x02030a); scene.fog=new THREE.Fog(0x02030a,18,50); const camera=new THREE.PerspectiveCamera(32,innerWidth/innerHeight,.1,200); const renderer=new THREE.WebGLRenderer({antialias:true}); renderer.setSize(innerWidth,innerHeight); renderer.setPixelRatio(Math.min(2,devicePixelRatio||1)); mount.appendChild(renderer.domElement); scene.add(new THREE.HemisphereLight(0x9bbdff,0x120816,1.2)); const keyLight=new THREE.DirectionalLight(0xffffff,1.6);keyLight.position.set(8,14,7);scene.add(keyLight); // ── Helpers ─────────────────────────────────────────────────────────────── function w2v(p,y=0){return new THREE.Vector3(p.x*scale,y,p.y*scale);} function polar(a,r){return{x:Math.cos(a)*r,y:Math.sin(a)*r};} function add2(a,b){return{x:a.x+b.x,y:a.y+b.y};} function sub2(a,b){return{x:a.x-b.x,y:a.y-b.y};} function mul2(a,n){return{x:a.x*n,y:a.y*n};} function len2(a){return Math.hypot(a.x,a.y);} function norm2(a){const l=len2(a)||1;return{x:a.x/l,y:a.y/l};} function angleOf(p){return Math.atan2(p.y,p.x);} function normalizeAngle(a){while(a<=-Math.PI)a+=Math.PI*2;while(a>Math.PI)a-=Math.PI*2;return a;} function angularDelta(f,t){return normalizeAngle(t-f);} function rotate2(v,a){const c=Math.cos(a),s=Math.sin(a);return{x:v.x*c-v.y*s,y:v.x*s+v.y*c};} function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v));} function lerp(a,b,p){return a+(b-a)*p;} function smooth(p){p=clamp(p,0,1);return p*p*(3-2*p);} function rand(){seed=(seed*1664525+1013904223)>>>0;return seed/4294967296;} function makeRing(radius,color,opacity,tube=0.04){ const m=new THREE.Mesh(new THREE.TorusGeometry(radius*scale,tube,12,96), new THREE.MeshBasicMaterial({color,transparent:true,opacity})); m.rotation.x=Math.PI/2;return m;} function makeCyl(color,emissive=0,rT=0.19,rB=0.19,h=0.68){ return new THREE.Mesh(new THREE.CylinderGeometry(rT,rB,h,8), new THREE.MeshStandardMaterial({color,emissive,roughness:.6,metalness:.1}));} function orientCylBetween(m,s3,e3){ const dir=e3.clone().sub(s3),len=dir.length(); if(len<.001){m.visible=false;return;} m.scale.y=len;m.position.copy(s3.clone().add(e3).multiplyScalar(.5)); m.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),dir.normalize());m.visible=true;} function showMessage(text,type,dur=3.2){ messageEl.textContent=text;messageEl.className=`show ${type}`;messageExpiry=time+dur;} function penalize(type,amount,text,msgType='danger'){ score-=amount;penalties[type]=(penalties[type]||0)+amount; showMessage(text,msgType);} // ── Timing ──────────────────────────────────────────────────────────────── function raidAngleAt(t){ let a=-Math.PI/2; const moveDelay=1.0; // raid holds still for 1s after Heaven cast starts for(let i=0;i<3;i++){ const ht=phase.firstHeaven+i*phase.heavenSpacing;if(t<=ht)break; const dur=phase.heavenRampDuration+phase.heavenLaserDuration; a+=phase.raidDirection*phase.raidAdvanceDegrees*Math.PI/180*clamp((t-ht-moveDelay)/(dur-moveDelay),0,1); if(t>=ht+dur)a=-Math.PI/2+phase.raidDirection*(i+1)*phase.raidAdvanceDegrees*Math.PI/180;} return a;} function raidPosAt(t){return polar(raidAngleAt(t),world.stackDistance);} function currentHeaven(t){ for(let i=0;i<3;i++){const s=phase.firstHeaven+i*phase.heavenSpacing,ls=s+phase.heavenRampDuration,e=ls+phase.heavenLaserDuration; if(t>=s&&t<=e)return{index:i,start:s,laserStart:ls,end:e};}return null;} function heavenCountdownAt(t){ for(let i=0;i<3;i++){const s=phase.firstHeaven+i*phase.heavenSpacing;if(t>=s-3&&t=s-1.2&&t<=s+9)return{index:c,start:s,end:s+9};}return null;} function directedRaidDelta(f,t){return normalizeAngle((t-f)*phase.raidDirection);} function stunLiftAt(t){ const s=phase.reintegrationCast,e=s+phase.stunDuration;if(te)return 0; const dt=t-s,p=dt<=5.5?smooth(dt/5.5):1-smooth((dt-5.5)/1.5);return p*5.8;} function coneActive(t){ const c=activeStarsplinterCycle(t);if(!c)return false; const s=c.start+phase.tankConeDelay,e=s+phase.tankConeDuration;return t>=s&&t<=e;} // ── Splinter layout ──────────────────────────────────────────────────────── function splinterLayout(stackPos,orientB){ const r=norm2(stackPos); return splinterSides.map((side,i)=>{ const t={x:-r.y*side,y:r.x*side},p=splinterProfiles[i]; let d=add2(stackPos,add2(mul2(t,p.tangential),mul2(r,p.radial))); if(orientB){const rel=sub2(d,stackPos);d=add2(stackPos,rotate2(rel,28*Math.PI/180*side));} const rel=sub2(d,stackPos),dist=len2(rel),maxR=world.lightRadius-4; if(dist>maxR)d=add2(stackPos,mul2(norm2(rel),maxR)); return d;});} function shardAnglesForDrop(stackPos,drop,orientB){ const da=angleOf(sub2(stackPos,drop)),ex=orientB?Math.PI/4:Math.PI/6; // orientA: one ray points directly toward the stack (da+0); orientB: rotated 45Β° return Array.from({length:6},(_,i)=>da+ex+i*Math.PI/3);} function splinterState(t,ci,si,stackPos,drop){ const apply=phase.firstStarsplinter+ci*phase.starsplinterSpacing+si*phase.starsApplyGap; const explode=apply+phase.starsExplosionDelay,retEnd=explode+phase.returnDuration; if(tretEnd)return null; let pos=stackPos,sv=false,ex=false; if(t=.68; }else if(tworld.shardLength)return false; return len2(sub2(target,add2(drop,mul2(dir,along))))polar(-Math.PI/2+phase.raidDirection*i*phase.raidAdvanceDegrees*Math.PI/180,world.stackDistance)); function markerArrivalTime(i){ if(i===0)return phase.firstHeaven; return phase.firstHeaven+(i-1)*phase.heavenSpacing+phase.heavenRampDuration+phase.heavenLaserDuration;} const worldMarkers=wmpPositions.map((pos,i)=>{ const col=MARKER_COLORS[i]; const mat=new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:.35,transparent:true,opacity:.5,roughness:.4}); const mesh=new THREE.Mesh(new THREE.CylinderGeometry(.44,.52,2.1,16),mat); mesh.position.copy(w2v(pos,1.05));scene.add(mesh); const ring=makeRing(16,col,.85,.055);ring.position.copy(w2v(pos,.03));scene.add(ring); return{mesh,ring,mat,ringMat:ring.material};}); // ── Floor / room ────────────────────────────────────────────────────────── const floorMesh=new THREE.Mesh(new THREE.CylinderGeometry(world.roomRadius*scale,world.roomRadius*scale,.05,160), new THREE.MeshStandardMaterial({color:0x05060d,roughness:.92,metalness:.02})); floorMesh.position.y=-.04;scene.add(floorMesh); scene.add(makeRing(world.roomRadius,0xffffff,.08,.022)); const floorLineMat=new THREE.LineBasicMaterial({color:0x85d8ff,transparent:true,opacity:.18}); for(let r=55;r<=world.roomRadius-30;r+=38){const rg=makeRing(r,0x85d8ff,.14,.010);rg.position.y=.025;scene.add(rg);} for(let i=0;i<24;i++){const a=i*Math.PI/12; scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([w2v(polar(a,42),.035),w2v(polar(a,world.roomRadius-10),.035)]),floorLineMat));} // ── Boss ────────────────────────────────────────────────────────────────── const boss=new THREE.Mesh(new THREE.SphereGeometry(world.bossRadius*scale,32,18), new THREE.MeshStandardMaterial({color:0x6f42c1,emissive:0x2c104f,roughness:.4})); boss.position.set(0,world.bossRadius*scale,0);scene.add(boss); // ── Light zone ──────────────────────────────────────────────────────────── const lightRing=makeRing(world.lightRadius,0x55ccff,1.0,.10);scene.add(lightRing); const lightRingGlow=makeRing(world.lightRadius,0x88ddff,.28,.22);scene.add(lightRingGlow); const stackGlow=makeRing(7,0xff9900,.85,.048);scene.add(stackGlow); // Warm disc: red inner fading to yellow β€” two layered discs const lightDiscRed=new THREE.Mesh(new THREE.CircleGeometry(world.lightRadius*scale*.55,64), new THREE.MeshBasicMaterial({color:0xff2200,transparent:true,opacity:.32,depthWrite:false})); lightDiscRed.renderOrder=2;lightDiscRed.rotation.x=-Math.PI/2;lightDiscRed.visible=false;scene.add(lightDiscRed); const lightDisc=new THREE.Mesh(new THREE.CircleGeometry(world.lightRadius*scale,64), new THREE.MeshBasicMaterial({color:0xffaa00,transparent:true,opacity:.26,depthWrite:false})); lightDisc.renderOrder=1;lightDisc.rotation.x=-Math.PI/2;lightDisc.visible=false;scene.add(lightDisc); // ── OT cylinder + cone ──────────────────────────────────────────────────── const otCyl=makeCyl(0xffd45a,0x553600,.21,.21,.72);scene.add(otCyl); const otConeMesh=(()=>{ const ha=Math.PI/4,n=24,r=world.coneLength*scale,pts=[]; for(let i=0;i{ for(let j=0;j{const r=makeRing(12,0x69d6ff,.8,.035);r.visible=false;scene.add(r);return r;}); let beamTip2d={x:0,y:0}; // ── Shard cylinders ─────────────────────────────────────────────────────── // Shard: tapered β€” thick at player end (radiusBottom/-Y), needle tip at far end (radiusTop/+Y) // Two layers: bright cyan inner + dark blue outer glow const shardInnerMat=new THREE.MeshBasicMaterial({color:0xaaeeff,transparent:true,opacity:.97,depthWrite:false}); const shardOuterMat=new THREE.MeshBasicMaterial({color:0x1033cc,transparent:true,opacity:.52,depthWrite:false}); const shardInners=Array.from({length:18},()=>{ const m=new THREE.Mesh(new THREE.CylinderGeometry(.008,.11,1,7),shardInnerMat); m.visible=false;m.renderOrder=2;scene.add(m);return m;}); const shardOuters=Array.from({length:18},()=>{ const m=new THREE.Mesh(new THREE.CylinderGeometry(.028,.20,1,7),shardOuterMat); m.visible=false;m.renderOrder=1;scene.add(m);return m;}); function setShardCyl(idx,a,b){ orientCylBetween(shardInners[idx],w2v(a,.12),w2v(b,.12)); orientCylBetween(shardOuters[idx],w2v(a,.12),w2v(b,.12));} // ── Pulse triangles ─────────────────────────────────────────────────────── const triGeom=new THREE.BufferGeometry(); triGeom.setAttribute("position",new THREE.Float32BufferAttribute([0,.04,.48,-.22,.04,-.25,.22,.04,-.25],3)); triGeom.computeVertexNormals(); const pulseMat=new THREE.MeshBasicMaterial({color:0xff2a43,side:THREE.DoubleSide}); const pulseTriangles=Array.from({length:18},()=>{const t=new THREE.Mesh(triGeom,pulseMat);t.visible=false;scene.add(t);return t;}); // ── Splinter markers ────────────────────────────────────────────────────── const splinterMeshes=[0,1,2].map(()=>{ const m=new THREE.Mesh(new THREE.SphereGeometry(.28,18,12), new THREE.MeshStandardMaterial({color:0xff5f86,emissive:0x4f0619,roughness:.35})); scene.add(m);return m;}); // ── Add materials ───────────────────────────────────────────────────────── const mPurp=new THREE.MeshStandardMaterial({color:0x9b69d7,emissive:0x211039,roughness:.45}); const mPink=new THREE.MeshStandardMaterial({color:0xff5f86,emissive:0x3d0614,roughness:.45}); const mWhite=new THREE.MeshStandardMaterial({color:0xffffff,emissive:0x777777,roughness:.25}); // ── Add logic ───────────────────────────────────────────────────────────── function offTankPos(sp){return polar(angleOf(sp),world.offTankDistance);} function addThreatensRaidPath(a,heaven,t){ if(!heaven)return false; const pl=phase.raidAdvanceDegrees*Math.PI/180,sa=-Math.PI/2+phase.raidDirection*heaven.index*pl,ea=sa+phase.raidDirection*pl; const addA=angleOf(a.pos),ahead=directedRaidDelta(raidAngleAt(t),addA),rem=Math.max(0,directedRaidDelta(raidAngleAt(t),ea)); const pp=directedRaidDelta(sa,addA),r=len2(a.pos),ttr=(world.stackDistance-r)/Math.max(1,a.speed); return ahead>=0&&ahead<=rem+10*Math.PI/180&&pp>=-0.001&&pp<=pl+0.001&&r-.7&&ttr<(heaven.end-t)+2.0;} function immunityPos(t,sp){ const h=currentHeaven(t),home=add2(sp,mul2(norm2(sp),-28));if(!h)return home; const threat=adds.filter(a=>addThreatensRaidPath(a,h,t)).sort((a,b)=>Math.abs(world.stackDistance-len2(a.pos))-Math.abs(world.stackDistance-len2(b.pos)))[0]; const rad=norm2(sp),fs=h.index===1?-1:1,fb=add2(home,mul2({x:-rad.y*fs,y:rad.x*fs},48)); const intercept=threat?add2(threat.pos,mul2(threat.dir,20)):fb; const mp=smooth(clamp((t-h.start)/2.2,0,1)),rp=smooth(clamp((t-(h.end-1.4))/1.4,0,1)); const active={x:lerp(home.x,intercept.x,mp),y:lerp(home.y,intercept.y,mp)}; return{x:lerp(active.x,home.x,rp),y:lerp(active.y,home.y,rp)};} function soakAdd(a,t,soaker){ if(t-a.lastSoakedAt<1.2)return; // 10% chance to nudge direction on first soak if(t>=a.soakedUntil&&Math.random()<0.1){ const away=norm2(sub2(a.pos,soaker)); a.dir=norm2(rotate2(a.dir,clamp(angularDelta(angleOf(a.dir),angleOf(away)),-Math.PI/4,Math.PI/4)));} a.soakedUntil=t+1.2;a.lastSoakedAt=t;} function spawnAdd(t,wave,fixedAngle){ const a=fixedAngle!==undefined?fixedAngle:rand()*Math.PI*2; const s=polar(a,world.bossRadius+8); const mesh=new THREE.Mesh(new THREE.BoxGeometry(wave?.95:.80,wave?.22:.18,wave?.60:.50),wave?mPink:mPurp); scene.add(mesh); adds.push({id:nextAddId++,pos:s,dir:{x:Math.cos(a),y:Math.sin(a)}, speed:(wave?phase.shaWaveSpeed:phase.shaSpeed)*(0.82+rand()*.36), wave,wobble:rand()*Math.PI*2,soakedUntil:0,lastSoakedAt:-999,mesh});} function updateAdds(t,dt,sp,otp,ip){ if(t=cycle.start&&!waveSpawned.has(cycle.index)){for(let i=0;i<36;i++)spawnAdd(t,true,i*(Math.PI/18));waveSpawned.add(cycle.index);} if(t-lastAddTime>=phase.shaBaseInterval){spawnAdd(t,false);lastAddTime=t;} for(const a of adds){ a.pos=add2(a.pos,mul2(a.dir,a.speed*dt)); if(selectedRole==='tank'){ if(len2(sub2(a.pos,playerPos))<22){ if(t-a.lastSoakedAt>=1.2&&t>defExpiry){ score-=5;penalties.noDefensive=(penalties.noDefensive||0)+5; showMessage('NO DEFENSIVE! βˆ’5','bad',2);} soakAdd(a,t,playerPos);} }else if(len2(sub2(a.pos,otp))<22)soakAdd(a,t,otp); if(currentHeaven(t)&&len2(sub2(a.pos,ip))<20)soakAdd(a,t,ip); // Direction only changes via soakAdd a.mesh.material=t{const keep=len2(a.pos)7;if(!keep)scene.remove(a.mesh);return keep;});} // ── Cone kills ──────────────────────────────────────────────────────────── function fireConekills(t,conePos,coneFwd){ const otp=conePos,fwd=coneFwd; adds=adds.filter(a=>{ const rel=sub2(a.pos,otp); const along=rel.x*fwd.x+rel.y*fwd.y; if(along<0||along>world.coneLength)return true; const side=Math.abs(rel.x*(-fwd.y)+rel.y*fwd.x); if(side>along*Math.tan(Math.PI/4)+12)return true; a.mesh.material=new THREE.MeshBasicMaterial({color:0xffffff,transparent:true,opacity:1}); dyingAdds.push({pos:{...a.pos},startT:t,mesh:a.mesh}); return false;});} function updateConeKills(t,stackPos){ if(selectedRole==='tank'){ if(!eabConePos||t>=eabConeExpiry||t<=phase.reintegrationCast+phase.stunDuration)return; if(!eabConeFired){eabConeFired=true;fireConekills(t,eabConePos,eabConeDir);} }else{ if(!coneActive(t))return; const c=activeStarsplinterCycle(t);if(!c)return; const key=`cone-${c.index}`; if(firedConeKills.has(key))return; firedConeKills.add(key); fireConekills(t,offTankPos(stackPos),norm2(mul2(stackPos,-1))); }} function updateDyingAdds(t){ for(let i=dyingAdds.length-1;i>=0;i--){ const d=dyingAdds[i],p=clamp((t-d.startT)/.35,0,1); if(p>=1){scene.remove(d.mesh);dyingAdds.splice(i,1);continue;} d.mesh.position.copy(w2v(d.pos,.42+p*.9)); d.mesh.material.opacity=1-p; d.mesh.scale.setScalar(1+p*2.5);}} // ── Player ──────────────────────────────────────────────────────────────── function updatePlayer(t,dt,stackPos,liftY){ const dur=t>=phase.reintegrationCast&&t<=phase.reintegrationCast+phase.stunDuration; const aft=t>phase.reintegrationCast+phase.stunDuration; playerCyl.visible=playerRing.visible=dur||aft;if(!playerCyl.visible)return; // Update facing only during stun and Heaven movement β€” locked otherwise if(dur||currentHeaven(t)){ const toBoss={x:-playerPos.x,y:-playerPos.y}; if(len2(toBoss)>1)playerFacingDir=norm2(toBoss);} if(aft&&running){ const spd=(selectedRole==='light'?45.5:35)*dt,fwd=playerFacingDir,rgt={x:-fwd.y,y:fwd.x}; if(keys['z'])playerPos=add2(playerPos,mul2(fwd,spd)); if(keys['s'])playerPos=add2(playerPos,mul2({x:-fwd.x,y:-fwd.y},spd)); if(keys['q'])playerPos=add2(playerPos,mul2({x:fwd.y,y:-fwd.x},spd)); if(keys['d'])playerPos=add2(playerPos,mul2(rgt,spd));} if(dur)playerPos={...stackPos}; const baseY=.34+(dur?liftY:0); playerCyl.position.copy(w2v(playerPos,baseY)); playerRing.position.copy(w2v(playerPos,.04+(dur?liftY:0)));} const aiWander=Array.from({length:17},()=>({active:false,startTime:0,endTime:0,fromPos:{x:0,y:0},target:{x:0,y:0}})); // ── AI players ──────────────────────────────────────────────────────────── function updateAIPlayers(t,stackPos,liftY){ const dur=t>=phase.reintegrationCast&&t<=phase.reintegrationCast+phase.stunDuration; const aft=t>phase.reintegrationCast+phase.stunDuration; const show=dur||aft; const cycle=activeStarsplinterCycle(t); const playerSP=cycle?getCycleAssignment(cycle.index):-1; const orientB=cycle?getCycleOrientation(cycle.index):0; const drops=cycle?splinterLayout(stackPos,orientB):null; // Trigger random wander for a non-SP AI if(aft&&running&&t>=nextWanderTime){ const cands=aiPlayers.map((a,i)=>i).filter(i=>aiPlayers[i].spIndex===null&&!aiWander[i].active); if(cands.length){ const idx=cands[Math.floor(Math.random()*cands.length)]; const ang=Math.random()*Math.PI*2,dist=world.lightRadius*.5+Math.random()*world.lightRadius*.3; Object.assign(aiWander[idx],{active:true,startTime:t,endTime:t+0.9+Math.random()*.6, fromPos:{...aiPlayers[idx].currentPos},target:add2(stackPos,polar(ang,dist))});} nextWanderTime=t+12+Math.random()*15;} aiPlayers.forEach((ai,i)=>{ ai.mesh.visible=show;if(!show)return; let pos; if(ai.spIndex!==null&&cycle&&ai.spIndex!==playerSP){ const eDrop=ai.spIndex===2?getAdjustedSP2Drop(cycle.index,stackPos,drops,orientB):getCycleSafeDrop(cycle.index,ai.spIndex,stackPos,drops,orientB); const st=splinterState(t,cycle.index,ai.spIndex,stackPos,eDrop); pos=st?st.pos:add2(stackPos,ai.baseOffset); // Chaotic fakeout: briefly move toward wrong side then self-correct if(chaoticCheck.checked&&ai.spIndex<2&&drops){ const fo=getCycleFakeout(cycle.index,drops)[ai.spIndex]; if(fo){const applyI=phase.firstStarsplinter+cycle.index*phase.starsplinterSpacing+ai.spIndex*phase.starsApplyGap; if(t>=applyI&&t=tot){w.active=false;} else if(elc.visible=false);shardOuters.forEach(c=>c.visible=false); pulseTriangles.forEach(p=>p.visible=false); splinterMeshes.forEach(m=>m.visible=false); targetRing.visible=false; const cycle=activeStarsplinterCycle(t);if(!cycle)return; const playerSP=getCycleAssignment(cycle.index); const orientB=getCycleOrientation(cycle.index); const drops=splinterLayout(stackPos,orientB); let li=0; for(let i=0;i<3;i++){ if(i===playerSP){ const apply=phase.firstStarsplinter+cycle.index*phase.starsplinterSpacing+i*phase.starsApplyGap; const explode=apply+phase.starsExplosionDelay,retEnd=explode+phase.returnDuration; // If munched during active splinter, shift time forward to explosion const tEff=(playerSplinterForceExplodeAt=apply&&tretEnd){li+=6;continue;} // Show target ring only if helper enabled if(helperCheck.checked&&tEff=apply&&tEff=explode&&tEff=h.laserStart); if(!active){ beamCyl.visible=beamGlowCyl.visible=false; beamPulses.forEach(r=>r.visible=false); laserFlashMat.opacity=0;return;} const angle=raidAngleAt(t)-phase.raidDirection*40*Math.PI/180; beamTip2d=polar(angle,world.stackDistance); // Beam from top of boss sphere to floor at tip const bossTop=new THREE.Vector3(0,world.bossRadius*scale*2,0); const tipFloor=w2v(beamTip2d,0); orientCylBetween(beamCyl,bossTop,tipFloor); orientCylBetween(beamGlowCyl,bossTop,tipFloor); beamCyl.visible=beamGlowCyl.visible=true; // Pulse rings at tip const pulse=((t-h.laserStart)%.8)/.8; beamPulses.forEach((r,i)=>{ r.visible=true;const rp=(pulse+i/3)%1; r.position.copy(w2v(beamTip2d,.08));r.scale.setScalar(.55+rp*2.2);r.material.opacity=.72*(1-rp);}); // Flash room red once per second const flashPhase=((t-h.laserStart)%1.0); laserFlashMat.opacity=flashPhase<.1?smooth(flashPhase/.1)*.22:flashPhase<.22?smooth(1-(flashPhase-.1)/.12)*.22:0;} // ── OT cone ─────────────────────────────────────────────────────────────── function applyConeWave(mesh,progress){ // Scale along forward axis (X) to animate wave sweeping out const p=clamp(progress,0,1); mesh.scale.set(p,1,1); mesh.material.opacity=0.55*(1-p*0.6);} function updateOTCone(t,stackPos){ if(selectedRole==='tank'){ const eabActive=eabConePos&&tphase.reintegrationCast+phase.stunDuration; otConeMesh.visible=!!eabActive; if(eabActive){ const p=(t-(eabConeExpiry-0.5))/0.5; otConeMesh.position.copy(w2v(eabConePos,0)); const fwd=new THREE.Vector3(eabConeDir.x,0,eabConeDir.y).normalize(); otConeMesh.quaternion.setFromUnitVectors(new THREE.Vector3(1,0,0),fwd); applyConeWave(otConeMesh,p);} return;} const active=coneActive(t)&&t>phase.reintegrationCast+phase.stunDuration; otConeMesh.visible=active;if(!active){otConeMesh.scale.set(1,1,1);return;} const c=activeStarsplinterCycle(t); const coneStart=c?c.start+phase.tankConeDelay:t; const p=(t-coneStart)/phase.tankConeDuration; const otp=offTankPos(stackPos),n=norm2(otp); otConeMesh.position.copy(w2v(otp,0)); const to=new THREE.Vector3(-n.x,0,-n.y).normalize(); otConeMesh.quaternion.setFromUnitVectors(new THREE.Vector3(1,0,0),to); applyConeWave(otConeMesh,p);} // ── World markers ───────────────────────────────────────────────────────── function updateWorldMarkers(t){ if(!markersCheck.checked){worldMarkers.forEach(m=>{m.mesh.visible=m.ring.visible=false;});return;} const stunEnd=phase.reintegrationCast+phase.stunDuration; worldMarkers.forEach((m,i)=>{ const arr=markerArrivalTime(i),f0=arr-.5,f1=arr+1.8; if(t>=f1||(i===0&&t>stunEnd)){m.mesh.visible=m.ring.visible=false;return;} m.mesh.visible=m.ring.visible=true; const alpha=t>=f0?1-smooth((t-f0)/(f1-f0)):1; m.mat.opacity=.5*alpha;m.ringMat.opacity=.85*alpha; const isNext=i>0&&t=markerArrivalTime(i-1)); m.mat.emissiveIntensity=isNext?.35+.2*Math.sin(t*5):.35;});} // ── Stun ───────────────────────────────────────────────────────────────── function updateStun(t){ const s=phase.reintegrationCast,e=s+phase.stunDuration,active=t>=s&&t<=e; stunField.visible=active;if(!active)return; const pulse=.5+.5*Math.sin((t-s)*Math.PI*3.4); stunFieldMat.color.setHex(pulse>.5?0xff2a43:0x5cc8ff);stunFieldMat.opacity=.13+pulse*.16;} // ── Boss cast bar (Heaven & Hell) ───────────────────────────────────────── const _bossTop=new THREE.Vector3(0,world.bossRadius*scale*0.5,0); function updateStunHints(t){ const s=phase.reintegrationCast,e=s+phase.stunDuration; // North hint: fade in t=0..0.5, hold, fade out t=2.5..3.0 if(reintHintEl){ let op=0; if(t>=0&&t<=e){ if(t<0.5)op=t/0.5; else if(t<2.5)op=1; else if(t<3.0)op=1-(t-2.5)/0.5; else op=0; } reintHintEl.style.opacity=String(op); } const lines=document.querySelectorAll('#stun-hints .hint-line'); if(!lines.length)return; const active=t>=2.5&&t<=e; const fadeOutStart=e-1.5; lines.forEach((line,i)=>{ if(!active){line.style.opacity='0';return;} const fadeInAt=2.5+i*1.0; if(t=fadeOutStart){ const p=Math.min(1,(t-fadeOutStart)/0.4); line.style.opacity=String(1-p); }else{ const p=Math.min(1,(t-fadeInAt)/0.35); line.style.opacity=String(p);}});} function updateBossCastBar(t){ const h=currentHeaven(t); if(!h){ // Show countdown to next Heaven and Hell let nextStart=Infinity; for(let i=0;i<3;i++){const s=phase.firstHeaven+i*phase.heavenSpacing;if(s>t){nextStart=s;break;}} if(nextStart===Infinity){bossCastbarEl.classList.remove("show");return;} const remaining=Math.max(0,nextStart-t); bcbLabelEl.textContent=`Heaven & Hell β€” ${remaining.toFixed(1)}s`; bcbFillEl.className="bcb-fill casting"; const elapsed=phase.heavenSpacing-(nextStart-(phase.firstHeaven>t?phase.firstHeaven:nextStart-phase.heavenSpacing))-remaining; bcbFillEl.style.width=`${clamp(1-remaining/phase.heavenSpacing,0,1)*100}%`; bossCastbarEl.classList.add("show");return;} const casting=tworld.lightRadius){anyOutside=true;break;}} if(anyOutside)score-=1.0*dt; // -5 score if player moves while any splinter is still in-flight // Window: cycle.start β†’ last splinter's retEnd = start + 2*applyGap + explosionDelay + returnDuration const sc=activeStarsplinterCycle(t); const lastRetEnd=sc?sc.start+2*phase.starsApplyGap+phase.starsExplosionDelay+phase.returnDuration:0; if(sc&&t>=sc.start&&tlightMovePenaltyExpiry){ score-=5;lightMovePenaltyExpiry=t+3; showMessage('DO NOT MOVE β€” STARSPLINTERS ACTIVE! βˆ’5','danger',2);} return;} const scoreRadius=heaven?world.lightRadius:12; const inLight=len2(sub2(playerPos,stackPos))<=scoreRadius; if(inLight){ const base=chaoticCheck.checked?1.15:(helperCheck.checked?.5:1); const gain=base*dt*Number(speedInput.value); score+=gain;safeAccum+=gain;}} function gradeScore(s){ if(s>=70)return{g:'A+',cls:'gAp',desc:'Flawless'}; if(s>=55)return{g:'A',cls:'gA',desc:'Excellent'}; if(s>=40)return{g:'B',cls:'gB',desc:'Good'}; if(s>=25)return{g:'C',cls:'gC',desc:'Adequate'}; if(s>=10)return{g:'D',cls:'gD',desc:'Rough'}; if(s>=0) return{g:'F',cls:'gF',desc:'Failed'}; return{g:'F-',cls:'gFm',desc:'Catastrophic'};} function showScoreboard(){ const rounded=Math.round(score); const gr=gradeScore(rounded); const modeLabels={easy:'⭐ EASY MODE',normal:'NORMAL',chaotic:'πŸ”₯ CHAOTIC PHASE 4 ATTEMPT'}; const roleSuffix={light:' Β· LIGHT HOLDER',tank:' Β· ADD TANK',dps:''}; const smodeEl=document.getElementById("smode"); smodeEl.textContent=(modeLabels[selectedMode]||'NORMAL')+(roleSuffix[selectedRole]||''); smodeEl.style.color=selectedMode==='chaotic'?'#ff9f1c':selectedMode==='easy'?'#a8e563':'var(--muted)'; document.getElementById("sgrade").textContent=gr.g; document.getElementById("sgrade").className=`score-grade ${gr.cls}`; document.getElementById("sfinal").textContent=`${rounded} pts β€” ${gr.desc}`; document.getElementById("sr-safe").textContent=`+${Math.round(safeAccum)}`; document.getElementById("sr-slice").textContent=`βˆ’${penalties.sliceOthers||0}`; document.getElementById("sr-sliced").textContent=`βˆ’${penalties.sliced||0}`; document.getElementById("sr-munch").textContent=`βˆ’${penalties.munched||0}`; document.getElementById("sr-midnight").textContent=`βˆ’${penalties.midnight||0}`; document.getElementById("sr-zap").textContent=`βˆ’${penalties.zapped||0}`; document.getElementById("sr-wrongside").textContent=`βˆ’${penalties.wrongSide||0}`; document.getElementById("sr-nodef").textContent=`βˆ’${penalties.noDefensive||0}`; document.getElementById("sr-noeab").textContent=`βˆ’${penalties.noEabClear||0}`; scoreboardEl.classList.add("show");scoreboardShown=true;} // ── Collision checks ────────────────────────────────────────────────────── function checkExplosions(t,stackPos){ const cycle=activeStarsplinterCycle(t);if(!cycle)return; const playerSP=getCycleAssignment(cycle.index),orientB=getCycleOrientation(cycle.index); const drops=splinterLayout(stackPos,orientB); for(let si=0;si<3;si++){ const apply=phase.firstStarsplinter+cycle.index*phase.starsplinterSpacing+si*phase.starsApplyGap; const explode=apply+phase.starsExplosionDelay,key=`${cycle.index}-${si}`; const tEff=(playerSplinterForceExplodeAt=apply&&t=explode&&tEff0){ const pen=count*10;score-=pen;penalties.sliceOthers=(penalties.sliceOthers||0)+pen; showMessage(`YOU SLICED ${count} PLAYER${count>1?'S':''}! βˆ’${pen}`,'bad');} const radial=norm2(stackPos),tangent={x:-radial.y,y:radial.x}; const lateral=(playerPos.x-stackPos.x)*tangent.x+(playerPos.y-stackPos.y)*tangent.y; const expectedSign=splinterSides[playerSP]; if(Math.abs(lateral)>20&&lateral*expectedSign<0){ score-=15;penalties.wrongSide+=15; const dirs=['LEFT','RIGHT','LEFT']; showMessage(`WRONG SIDE β€” should be ${dirs[playerSP]}! βˆ’15`,'bad',3);} }else{ const angles=shardAnglesForDrop(stackPos,exOrigin,orientB); for(const ang of angles){if(shardHitsPos(exOrigin,ang,playerPos)){ score-=10;penalties.sliced=(penalties.sliced||0)+10; showMessage('YOU GOT SLICED! βˆ’10','danger');break;}} if(chaoticCheck.checked){ let killedAny=false; aiPlayers.forEach((ai,idx)=>{ if(deadAI.has(idx)||!ai.mesh.visible)return; for(const ang of angles){if(shardHitsPos(exOrigin,ang,ai.currentPos,13)){deadAI.add(idx);ai.mesh.visible=false;killedAny=true;break;}}}); if(killedAny&&time-lastDeathMsgTime>2){ deathMsgEl.textContent=DEATH_REACTIONS[Math.floor(Math.random()*DEATH_REACTIONS.length)]; deathMsgEl.classList.add("show"); deathMsgExpiry=time+2.2;lastDeathMsgTime=time;} }}}}} function checkOnYouSP(t,stackPos){ const cycle=activeStarsplinterCycle(t);if(!cycle)return; const playerSP=getCycleAssignment(cycle.index); if(playerSP===-1)return; const apply=phase.firstStarsplinter+cycle.index*phase.starsplinterSpacing+playerSP*phase.starsApplyGap; const key=`onyou-${cycle.index}`; if(t>=apply&&th.end){checkedLasers.add(key);return;} if(len2(sub2(playerPos,beamTip2d))<32){ score-=100;penalties.zapped=(penalties.zapped||0)+100; showMessage('YOU GOT ZAPPED! βˆ’100','danger');checkedLasers.add(key);}} function checkMunch(t,stackPos){ if(selectedRole==='tank')return; if(t-lastMunchTime<3)return; if(len2(sub2(playerPos,stackPos))<18)return;// safe inside stack circle for(const a of adds){ if(a.wave||t=ap&&tworld.lightRadius){ score-=100;penalties.midnight=(penalties.midnight||0)+100; showMessage('MIDNIGHT FALLS! βˆ’100','danger',2.5);lastMidnightTime=t;}} // ── HUD / Raid call ─────────────────────────────────────────────────────── function updateRaidCall(t){ const countdown=heavenCountdownAt(t); const heaven=currentHeaven(t); const chaotic=chaoticCheck.checked; if(chaotic){ // SP bar hidden in chaos raidcallEl.classList.remove("show"); // Movement call: flip MOVE/WAIT during countdown, GO GO GO during ramp if(countdown){ if(t>=nextChaosMoveFlip){ chaosMoveText=CHAOS_MOVE_CALLS[Math.floor(Math.random()*CHAOS_MOVE_CALLS.length)]; nextChaosMoveFlip=t+0.32+Math.random()*0.48;} movecallEl.querySelector('.movelabel').textContent=chaosMoveText; movecountdownEl.textContent=''; movecallEl.classList.add("show"); }else if(heaven&&t=cycle.start-1.5){ raidcallEl.classList.add("show"); spSlotEls.forEach((el,i)=>{ const apply=phase.firstStarsplinter+cycle.index*phase.starsplinterSpacing+i*phase.starsApplyGap; const retEnd=apply+phase.starsExplosionDelay+phase.returnDuration; el.classList.toggle("stepping",t>=apply&&t<=retEnd);}); }else raidcallEl.classList.remove("show"); } if(time>messageExpiry)messageEl.className='';} function updateHud(t){ const stars=activeStarsplinterCycle(t),heaven=currentHeaven(t); let label="Holding"; if(t<=phase.reintegrationCast)label="Reintegration"; else if(t<=phase.reintegrationCast+phase.stunDuration)label="Stunned"; else if(stars&&t>=stars.start&&t<=stars.end)label="Starsplinters"; else if(heaven)label="Heaven and Hell"; clockEl.textContent=`${t.toFixed(1)}s`;stateEl.textContent=label; cycleEl.textContent=`${Math.min(4,Math.max(1,stars?stars.index+1:Math.floor((t-phase.firstStarsplinter)/phase.starsplinterSpacing)+1))} / 4`; scorePillEl.textContent=Math.round(score);} // ── Frame ───────────────────────────────────────────────────────────────── function frame(now){ const dt=Math.min(.08,(now-last)/1000);last=now; if(running){ time+=dt*Number(speedInput.value); if(time>=phase.duration){ time=phase.duration;running=false;playBtn.textContent="Play"; if(!scoreboardShown)showScoreboard();}} const stackPos=raidPosAt(time),otPos=offTankPos(stackPos); const immunePosNow=immunityPos(time,stackPos); const liftY=stunLiftAt(time); const dur=time>=phase.reintegrationCast&&time<=phase.reintegrationCast+phase.stunDuration; const aft=time>phase.reintegrationCast+phase.stunDuration; const showRaid=dur||aft; updateAdds(time,dt*Number(speedInput.value),stackPos,otPos,immunePosNow); updateDyingAdds(time); if(dur)playerPos={...stackPos}; stackGlow.visible=lightRing.visible=lightRingGlow.visible=lightDisc.visible=lightDiscRed.visible=showRaid; stackGlow.position.copy(w2v(stackPos,.05+liftY)); const lightCenter=selectedRole==='light'?playerPos:stackPos; lightRing.position.copy(w2v(lightCenter,.06+liftY)); lightRingGlow.position.copy(w2v(lightCenter,.06+liftY)); lightDisc.position.set(lightCenter.x*scale,.06+liftY,lightCenter.y*scale); lightDiscRed.position.set(lightCenter.x*scale,.065+liftY,lightCenter.y*scale); otCyl.visible=showRaid&&selectedRole!=='tank';otCyl.position.copy(w2v(otPos,.36+liftY)); immuneMesh.visible=Boolean(aft&Β€tHeaven(time)); immuneMesh.position.copy(w2v(immunePosNow,.34)); const heaven=currentHeaven(time); if(heaven)immuneMesh.material.color.setHex(immunityColors[heaven.index]); updateStun(time); updateBeam(time); updateSplinters(time,stackPos); updateAIPlayers(time,stackPos,liftY); updatePlayer(time,dt*Number(speedInput.value),stackPos,liftY); updateOTCone(time,stackPos); updateConeKills(time,stackPos); updateWorldMarkers(time); updateStunHints(time); updateBossCastBar(time); updateCastBar(time); updateCamera(liftY); // reintHintEl opacity managed by updateStunHints if(scorephase.firstStarsplinter+sc.index*phase.starsplinterSpacing+getCycleAssignment(sc.index)*phase.starsApplyGap+phase.starsExplosionDelay+phase.returnDuration) playerSplinterForceExplodeAt=Infinity;} checkOnYouSP(time,stackPos); checkZap(time); checkMunch(time,stackPos); // Add Tank: penalise βˆ’75 if EAB not used to clear red wave if(selectedRole==='tank'){ const wc=activeStarsplinterCycle(time); if(wc&&waveSpawned.has(wc.index)&&!checkedWavePenalties.has(wc.index)&&time>=wc.start+3.0){ checkedWavePenalties.add(wc.index); if(!tankEabWaveKillCycles.has(wc.index)){ score-=75;penalties.noEabClear=(penalties.noEabClear||0)+75; showMessage('ADDS HIT THE RAID! βˆ’75','danger',3);}}} checkMidnight(time,stackPos);} // ── Chaotic mode overlays ──────────────────────────────────────────────── if(chaoticCheck.checked&&running){ // World cup if(time>=nextWorldCupTime){ const c=CHAOTIC_CORNERS[Math.floor(Math.random()*4)]; Object.assign(worldCupEl.style,{top:c.top,left:c.left,right:c.right,bottom:c.bottom}); worldCupEl.textContent=WCGOALS[Math.floor(Math.random()*WCGOALS.length)]; worldCupEl.classList.add("show"); worldCupExpiry=time+3;nextWorldCupTime=time+12+Math.random()*14;} if(time>worldCupExpiry)worldCupEl.classList.remove("show"); // Guild drama if(time>=nextGuildDrama&&!gcDramaActive){ const leaver=GUILD_NAMES[Math.floor(Math.random()*GUILD_NAMES.length)]; const others=[...GUILD_NAMES].filter(n=>n!==leaver).sort(()=>Math.random()-.5); const resps=[...GUILD_RESPONSES].sort(()=>Math.random()-.5); const count=4+Math.floor(Math.random()*5); gcQueue=[{name:leaver,text:'has left the guild.',type:'leave',delay:0,added:false}]; for(let i=0;i{ if(!m.added&&el>=m.delay){ m.added=true; const d=document.createElement('div');d.className='gc-msg'; d.innerHTML=m.type==='leave' ?`${m.name} ${m.text}` :m.type==='offline' ?`${m.name} ${m.text}` :`${m.name}: ${m.text}`; gcMsgsEl.appendChild(d);}}); const last=gcQueue[gcQueue.length-1]; if(last.added&&time-gcDramaStart>last.delay+5){guildChatEl.classList.remove("show");gcDramaActive=false;} } // Discord notifications if(time>=nextDcTime){ dcSenderEl.textContent=DISCORD_NAMES[Math.floor(Math.random()*DISCORD_NAMES.length)]; dcTextEl.textContent=DISCORD_MSGS[Math.floor(Math.random()*DISCORD_MSGS.length)]; discordEl.classList.add("show"); dcExpiry=time+5;nextDcTime=time+18+Math.random()*20;} if(time>dcExpiry)discordEl.classList.remove("show"); // LUA error popup if(time>=nextLuaErrorTime&&!luaErrorShowing){ luaErrorBodyEl.textContent=LUA_ERRORS[Math.floor(Math.random()*LUA_ERRORS.length)]; luaErrorEl.classList.add("show");luaErrorShowing=true; nextLuaErrorTime=time+25+Math.random()*22;} // Gone offline guild event if(time>=nextOfflineTime&&!gcDramaActive){ const name=GUILD_NAMES[Math.floor(Math.random()*GUILD_NAMES.length)]; const resps=[...OFFLINE_RESPONSES].sort(()=>Math.random()-.5).slice(0,1+Math.floor(Math.random()*2)); const others=[...GUILD_NAMES].filter(n=>n!==name).sort(()=>Math.random()-.5); gcQueue=[{name,text:'has gone offline.',type:'offline',delay:0,added:false}]; resps.forEach((r,i)=>gcQueue.push({name:others[i],text:guildResp(others[i],r),type:'resp',delay:(i+1)*.9+Math.random()*.4,added:false})); gcDramaStart=time;gcDramaActive=true;gcMsgsEl.innerHTML='';guildChatEl.classList.add("show"); nextOfflineTime=time+28+Math.random()*22;} // Death message hide if(time>deathMsgExpiry)deathMsgEl.classList.remove("show"); // Easter egg lines for(const s of easterEggSchedule){ if(!s.fired&&time>=s.t){ s.fired=true; addGuildChatMsg(s.egg.name,s.egg.text); if(s.egg.reply)pendingGcReplies.push({fireAt:time+1.2+Math.random()*.8,name:s.egg.reply.name,text:s.egg.reply.text}); gcEggExpiry=time+7;guildChatEl.classList.add("show");}} for(let i=pendingGcReplies.length-1;i>=0;i--){ const r=pendingGcReplies[i]; if(time>=r.fireAt){addGuildChatMsg(r.name,r.text);gcEggExpiry=time+6;pendingGcReplies.splice(i,1);}} if(!gcDramaActive&&time>gcEggExpiry)guildChatEl.classList.remove("show"); }else{ worldCupEl.classList.remove("show"); guildChatEl.classList.remove("show"); discordEl.classList.remove("show"); gcDramaActive=false;} boss.rotation.y+=dt*.35; renderer.render(scene,camera); requestAnimationFrame(frame);} function doRestart(){ for(const a of adds)scene.remove(a.mesh); for(const d of dyingAdds)scene.remove(d.mesh); adds=[];dyingAdds=[];time=0;seed=5;lastAddTime=-999;nextAddId=1;waveSpawned=new Set(); Object.keys(cycleAssignments).forEach(k=>delete cycleAssignments[k]); Object.keys(cycleOrientations).forEach(k=>delete cycleOrientations[k]); Object.keys(cycleDelays).forEach(k=>delete cycleDelays[k]); Object.keys(cycleAdjustedDrops).forEach(k=>delete cycleAdjustedDrops[k]); Object.keys(cycleFakeouts).forEach(k=>delete cycleFakeouts[k]); Object.keys(cycleSafeDrops).forEach(k=>delete cycleSafeDrops[k]); aiWander.forEach(w=>w.active=false);nextWanderTime=20+Math.random()*12; checkedExplosions.clear();checkedLasers.clear();firedConeKills.clear(); tankEabWaveKillCycles.clear();checkedWavePenalties.clear(); playerPos={x:0,y:-world.stackDistance};playerFacingDir={x:0,y:1}; messageExpiry=0;messageEl.className='';lastMunchTime=-999;lastMidnightTime=-999; score=0;safeAccum=0;Object.keys(penalties).forEach(k=>penalties[k]=0);scoreboardShown=false; deadAI.clear();aiPlayers.forEach(ai=>ai.mesh.visible=true); worldCupExpiry=-999;nextWorldCupTime=18+Math.random()*8;worldCupEl.classList.remove("show"); gcQueue=[];gcDramaActive=false;gcMsgsEl.innerHTML='';guildChatEl.classList.remove("show");nextGuildDrama=22+Math.random()*8; dcExpiry=-999;nextDcTime=35+Math.random()*10;discordEl.classList.remove("show"); chaosMoveText='WAIT!!';nextChaosMoveFlip=-999; luaErrorEl.classList.remove("show");luaErrorShowing=false;nextLuaErrorTime=28+Math.random()*12; deathMsgEl.classList.remove("show");deathMsgExpiry=-999;lastDeathMsgTime=-999; nextOfflineTime=45+Math.random()*10; gcEggExpiry=-999;pendingGcReplies=[];buildEasterEggSchedule();buildStunHints(); scoreboardEl.classList.remove("show"); reintHintEl.style.opacity='0'; prevScore=0;penaltyFlashExpiry=-999;spFlashExpiry=-999;penaltyFlashEl.classList.remove("on");spFlashEl.classList.remove("on"); eabCooldownUntil=-Infinity;eabConePos=null;eabConeExpiry=-Infinity;eabConeFired=false; defCharges=3;defExpiry=-Infinity;lightMovePenaltyExpiry=-Infinity; document.getElementById('eab-icon').classList.remove('show'); document.getElementById('def-charges').classList.remove('show'); document.getElementById('def-bar-wrap').classList.remove('show'); document.getElementById('role-step').style.display='flex'; document.getElementById('diff-step').style.display='none'; document.getElementById('eab-bind-row').style.display='none'; document.getElementById('tank-continue-btn').style.display='none'; modeSelectEl.style.display='flex'; running=false;playBtn.textContent="Pause"; camera.position.set(0,Number(camHeightInput.value),-Number(camDistInput.value)*scale);last=performance.now();} const STUN_HINTS={ dps:[ 'Remain pixel-stacked with the group whenever possible.', 'Move to the correct side with Starsplinter. (ZQSD)', 'Don\'t get hit by Void Swarm adds.', 'Don\'t fall behind the raid during Heaven & Hell.' ], light:[ 'Stand still on top of the raid during Starsplinters.', 'Move during Heaven and Hell without leaving people behind. (ZQSD)', 'Don\'t get hit by Void Swarm adds.' ], tank:[ 'Remain slightly in front of the group at all times.', 'Use \'The Last Light\' action to clear adds in front of you.', 'Use a defensive to soak any adds that will hit the group.', 'Move during Heaven & Hell and help to soak adds.' ], chaotic:[ 'You have selected Chaos Mode. Good Luck.', 'Your raid is arguing about whether someone was in the wrong spot in P3.', 'Your Raid Leader tells you to calm down.', 'Your raid leader tells you that this is the easiest part of the fight.', 'Rage consumes you as someone disconnects.', 'Your UI has been throwing errors all pull.' ] }; function buildStunHints(){ const lines=chaoticCheck.checked?STUN_HINTS.chaotic:(STUN_HINTS[selectedRole]||STUN_HINTS.dps); const el=document.getElementById('stun-hints'); el.innerHTML=lines.map(l=>`
${l}
`).join('');} function startMode(mode){ selectedMode=mode; helperCheck.checked=(mode==='easy'); chaoticCheck.checked=(mode==='chaotic'); buildEasterEggSchedule(); buildStunHints(); modeSelectEl.style.display='none'; if(selectedRole==='tank'){ document.getElementById('eab-icon').classList.add('show'); document.getElementById('def-charges').classList.add('show'); defCharges=3;defExpiry=-Infinity;updateDefUI();} running=true;playBtn.textContent="Pause";last=performance.now();} // Defensive UI helper function updateDefUI(){ const pips=document.getElementById('def-pips'); pips.innerHTML=''; for(let i=0;i<3;i++){const d=document.createElement('div');d.className='def-pip'+(i>=defCharges?' spent':'');pips.appendChild(d);}} // Role step β†’ diff step const roleLabels={'dps':'DPS / Healer','light':'Light Holder','tank':'Add Tank'}; function selectRole(role){ selectedRole=role; document.getElementById('diff-role-label').textContent=roleLabels[role]+' Β· Choose difficulty'; document.getElementById('role-step').style.display='none'; document.getElementById('diff-step').style.display='flex';} document.getElementById("btn-role-dps").addEventListener("click",()=>selectRole('dps')); document.getElementById("btn-role-light").addEventListener("click",()=>selectRole('light')); document.getElementById("btn-role-tank").addEventListener("click",()=>{ document.getElementById('eab-bind-row').style.display='flex'; document.getElementById('tank-continue-btn').style.display='block'; selectedRole='tank';}); document.getElementById("tank-continue-btn").addEventListener("click",()=>selectRole('tank')); document.getElementById("btn-back-role").addEventListener("click",()=>{ document.getElementById('diff-step').style.display='none'; document.getElementById('role-step').style.display='flex';}); // EAB / Defensive keybind capture const eabKeyBtn=document.getElementById('eab-key-btn'); const defKeyBtn=document.getElementById('def-key-btn'); let capturingEab=false,capturingDef=false; eabKeyBtn.addEventListener("click",()=>{capturingEab=true;capturingDef=false;eabKeyBtn.textContent='…';eabKeyBtn.classList.add('capturing');}); defKeyBtn.addEventListener("click",()=>{capturingDef=true;capturingEab=false;defKeyBtn.textContent='…';defKeyBtn.classList.add('capturing');}); window.addEventListener("keydown",e=>{ if(capturingEab){e.preventDefault();eabKey=e.key.toLowerCase(); eabKeyBtn.textContent=e.key.toUpperCase();eabKeyBtn.classList.remove('capturing');capturingEab=false;return;} if(capturingDef){e.preventDefault();defKey=e.key.toLowerCase(); defKeyBtn.textContent=e.key.toUpperCase();document.getElementById('def-key-label').textContent=e.key.toUpperCase(); defKeyBtn.classList.remove('capturing');capturingDef=false;return;}},true); document.getElementById("btn-easy").addEventListener("click",()=>startMode('easy')); document.getElementById("btn-normal").addEventListener("click",()=>startMode('normal')); document.getElementById("btn-chaotic").addEventListener("click",()=>startMode('chaotic')); playBtn.addEventListener("click",()=>{running=!running;playBtn.textContent=running?"Pause":"Play";last=performance.now();}); restartBtn.addEventListener("click",doRestart); document.getElementById("sbRestart").addEventListener("click",doRestart); window.addEventListener("resize",()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);}); camera.position.set(0,CAM_H,-CAM_BACK*scale); requestAnimationFrame(frame); </script>

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors