// Downloads the live ocellus-sim bundle and remaps camera drag from middle mouse to right mouse. // Right tap (moved < 6px) still opens the context menu, on release instead of press. // ocellus-sim is GPL-3.0 (github.com/alessandrorodi/ocellus-sim). import { writeFileSync, mkdirSync, existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const UPSTREAM = "https://ocellus-sim.pages.dev"; const SITE = join(dirname(fileURLToPath(import.meta.url)), "site"); const DRILL = '(+new URLSearchParams(location.search).get("drill")||0)'; const patches = [ // Camera rotate on right (2) as well as middle (1). ["1===e.button&&this.clientCameraRotation.setMiddleMouseDown(!0)", "(1===e.button||2===e.button)&&this.clientCameraRotation.setMiddleMouseDown(!0)"], ["1===e.button&&this.clientCameraRotation.setMiddleMouseDown(!1)", "(1===e.button||2===e.button)&&this.clientCameraRotation.setMiddleMouseDown(!1)"], // Don't open the menu on right press; remember where the press happened. ["if(2===e.button&&this.rightClickDown(e),1!==e.button){", "if(2===e.button&&(this.__rc={x:e.clientX,y:e.clientY}),1!==e.button&&2!==e.button){"], // Open it on right release if the mouse didn't drag. ["e.prototype.leftClickUp=function(e){", "e.prototype.leftClickUp=function(e){if(2===e.button&&this.__rc){var rc=this.__rc;this.__rc=null;Math.abs(e.clientX-rc.x)+Math.abs(e.clientY-rc.y)<6&&this.rightClickDown(e)}"], // Team size (?team=1-5, default solo): boss HP uses the sim's own party scaling (upstream locks it to solo), and // damage you deal the boss counts once per team member, as if each teammate hits like you. All phases; the HP // floors still hold him. __lp skips the first tick after a phase change, when his HP was set by the script. ["party:1,startPhase:", "party:Math.min(5,Math.max(1,Math.round(+new URLSearchParams(location.search).get(\"team\")||1))),startPhase:"], ["afterHits(e){const t=this.phase?.hpFloor()??0;", "afterHits(e){const m=this.options.party||1;if(m>1&&e.__lp===this.phase&&e.__lh>e.currentStats.hitpoint){e.currentStats.hitpoint=Math.max(0,e.currentStats.hitpoint-Math.round((e.__lh-e.currentStats.hitpoint)*(m-1)))}const t=this.phase?.hpFloor()??0;"], ["e.currentStats.hitpointthis.spawnRift());", "for(const e of(\"all\"===new URLSearchParams(location.search).get(\"rifts\")?r.rifts:r.rifts.slice(0,1)))this.at(e,()=>this.spawnRift());"], ["this.spawnRifts(Math.min(4,a+1))", "this.spawnRifts(\"all\"===new URLSearchParams(location.search).get(\"rifts\")?Math.min(4,a+1):1)"], // Melee amulet choice (?amulet=fury): blood fury in place of rancour, wherever rancour would be (worn or switch). // Wiki (2026-10-27): stats below; "20% chance to heal 30% of the damage dealt" on melee hits. It reuses rancour's // cache model since blood fury's isn't in the asset pack; only the worn necklace looks different in game. ["amuletOfRancour:()=>new r.AmuletOfRancour}", "amuletOfRancour:()=>{const a=new r.AmuletOfRancour;if(\"fury\"!==new URLSearchParams(location.search).get(\"amulet\"))return a;" + "a.__bf=!0;a.bonuses={attack:{stab:10,slash:10,crush:10,magic:10,range:10},defence:{stab:15,slash:15,crush:15,magic:15,range:15},other:{meleeStrength:8,rangedStrength:0,magicDamage:0,prayer:5},targetSpecific:{undead:0,slayer:0}};" + "Object.defineProperty(a,\"itemName\",{get:()=>\"Amulet of blood fury\"});Object.defineProperty(a,\"inventoryImage\",{get:()=>\"blood-fury.png\"});" + "const i=new Image;i.src=\"blood-fury.png\";a.inventorySprite=i;return a}}"], // Blood fury heal: each melee hitsplat you land (so each scythe hit) rolls 20% to heal floor(30%) of it, up to max HP. ["else{e.currentStats.hitpoint-=t.damage;var n=e.hitSound(t.damage>0);", "else{e.currentStats.hitpoint-=t.damage;var bf=t.from;t.damage>0&&bf&&bf.equipment&&bf.equipment.necklace&&bf.equipment.necklace.__bf&&(\"stab\"===t.attackStyle||\"slash\"===t.attackStyle||\"crush\"===t.attackStyle)&&Math.random()<.2&&(bf.currentStats.hitpoint=Math.min(bf.stats.hitpoint,bf.currentStats.hitpoint+Math.floor(.3*t.damage)));var n=e.hitSound(t.damage>0);"], // Spin drill (?drill=1|2|3 cuts): in phase 3 every special is the spin, with that many cuts and the matching rubble // timing (1 = first quadrant's, 2-3 = later ones'); his HP stays full so the quadrant never falls; each cut reports its result only (no cues). ["const e=this.specials%2==0?\"spin\":this.nextOther();", `const e=(${DRILL}||this.specials%2==0)?"spin":this.nextOther();`], ["e.after(0===this.segment?i.P3.debrisIntoSpin.first:i.P3.debrisIntoSpin.later,", `e.after(0===this.segment&&!(${DRILL}>1)?i.P3.debrisIntoSpin.first:i.P3.debrisIntoSpin.later,`], ["const s=this.fight.destroyed.size;for(let n=0;n<=s;n++){", `const s=(${DRILL}||this.fight.destroyed.size+1)-1;this.__db=0,this.__sv=-1;for(let n=0;n<=s;n++){`], ["e.after(c-1,()=>{o()&&null!==e.player.protection&&(e.player.prayerController", "e.after(c-1,()=>{o()&&null!==e.player.protection&&(this.__sv=n,e.player.prayerController"], ["l&&(this.stationary=!1),(0,a.reach)(e.playerTile,this.boss.tile)>8||", `l&&(this.stationary=!1),${DRILL}&&(()=>{const far=(0,a.reach)(e.playerTile,this.boss.tile)>8,ok=far||"melee"===e.player.protection;ok&&this.__db++;` + `e.message("Cut "+(n+1)+": "+(far?"out of reach":ok?"blocked":this.__sv===n?"HIT (prayer was on the tick before, severed)":"HIT (Protect from Melee not on)"),ok?r.CHAT.green:r.CHAT.red);` + `l&&e.message("Spin done: "+this.__db+"/"+(n+1)+" cuts blocked",this.__db===n+1?r.CHAT.green:r.CHAT.red)})(),(0,a.reach)(e.playerTile,this.boss.tile)>8||`], ["(0,a.same)(e.playerTile,t)&&e.hit({amount:()=>e.rng.roll(i.P3.debrisDamage)}),", `${DRILL}&&e.message((0,a.same)(e.playerTile,t)?"Rubble: hit":"Rubble: dodged",(0,a.same)(e.playerTile,t)?r.CHAT.red:r.CHAT.green),(0,a.same)(e.playerTile,t)&&e.hit({amount:()=>e.rng.roll(i.P3.debrisDamage)}),`], ["afterHits(e){const m=this.options.party||1;", `afterHits(e){"Phase 3"===this.phase?.label&&${DRILL}&&(e.currentStats.hitpoint=this.poolMax);const m=this.options.party||1;`], // Sidebar dropdown for the amulet, under Setup; takes effect on the next Start fight / Restart. ["a.default.createElement(\"option\",{value:\"melee\"},\"Melee (Scythe, Inquisitor's)\"))),", "a.default.createElement(\"option\",{value:\"melee\"},\"Melee (Scythe, Inquisitor's)\"))),a.default.createElement(\"label\",null,\"Melee amulet\",a.default.createElement(\"select\",{defaultValue:\"fury\"===new URLSearchParams(location.search).get(\"amulet\")?\"fury\":\"rancour\",onChange:ev=>{const U=new URL(location.href);U.searchParams.set(\"amulet\",ev.target.value);history.replaceState(history.state,\"\",U.href)}},a.default.createElement(\"option\",{value:\"rancour\"},\"Amulet of rancour\"),a.default.createElement(\"option\",{value:\"fury\"},\"Amulet of blood fury\")))," // Spin drill dropdown; picking one reloads straight into phase 3 practice. + `a.default.createElement("label",null,"Spin drill",a.default.createElement("select",{defaultValue:String(${DRILL}),onChange:ev=>{const U=new URL(location.href),v=ev.target.value;"0"===v?U.searchParams.delete("drill"):(U.searchParams.set("drill",v),U.searchParams.set("phase","3"),U.searchParams.set("practice","1"));location.assign(U.href)}},a.default.createElement("option",{value:"0"},"Off (normal fight)"),a.default.createElement("option",{value:"1"},"1 cut (quadrant 1)"),a.default.createElement("option",{value:"2"},"2 cuts (quadrant 2)"),a.default.createElement("option",{value:"3"},"3 cuts (quadrant 3)"))),` // Team size dropdown; boss HP is set when the fight is built, so picking one reloads. + `a.default.createElement("label",null,"Team size",a.default.createElement("select",{defaultValue:String(Math.min(5,Math.max(1,Math.round(+new URLSearchParams(location.search).get("team")||1)))),onChange:ev=>{const U=new URL(location.href),v=ev.target.value;"1"===v?U.searchParams.delete("team"):U.searchParams.set("team",v);U.searchParams.delete("p3dmg");location.assign(U.href)}},[["1","Solo (default)"],["2","Duo"],["3","Trio"],["4","4-man"],["5","5-man"]].map(([v,t])=>a.default.createElement("option",{key:v,value:v},t)))),`], ]; async function get(path) { const res = await fetch(UPSTREAM + path); if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`); return res; } mkdirSync(SITE, { recursive: true }); let js = await (await get("/main.js")).text(); for (const [from, to] of patches) { const n = js.split(from).length - 1; if (n !== 1) throw new Error(`expected 1 match, found ${n}: ${from}`); js = js.replace(from, to); } writeFileSync(join(SITE, "main.js"), js); const icon = await fetch("https://oldschool.runescape.wiki/images/Amulet_of_blood_fury.png", { headers: { "user-agent": "ocellus-sim-mirror (personal)" } }); if (!icon.ok) throw new Error(`blood fury icon: HTTP ${icon.status}`); writeFileSync(join(SITE, "blood-fury.png"), Buffer.from(await icon.arrayBuffer())); let html = await (await get("/")).text(); html = html.replace(/