sandboxels/mods/noita_demake.js

431 lines
14 KiB
JavaScript

// --- KEY STATE ---
var KA = false;
var KD = false;
var KW = false;
var KS = false;
var mouseX = 0;
var mouseY = 0;
var selectedSpell = 0;
keybinds["Digit1"] = function() {
selectedSpell = 0;
};
keybinds["Digit2"] = function() {
selectedSpell = 1;
};
keybinds["Digit3"] = function() {
selectedSpell = 2;
};
keybinds["Digit4"] = function() {
selectedSpell = 3;
};
document.addEventListener("keydown", function(e) {
if (e.key === "s" || e.key === "S") {
e.preventDefault();
KS = true;
}
const key = e.key;
if (key >= "0" && key <= "9") {
selectedSpell = key === "0" ? 9 : parseInt(key) - 1;
}
if (e.key === "a" || e.key === "A") KA = true;
if (e.key === "d" || e.key === "D") KD = true;
if (e.key === "w" || e.key === "W") KW = true;
if (e.key === "s" || e.key === "S") KS = true;
});
document.addEventListener("keyup", function(e) {
if (e.key === "s" || e.key === "S") {
e.preventDefault();
KS = false;
}
if (e.key === "a" || e.key === "A") KA = false;
if (e.key === "d" || e.key === "D") KD = false;
if (e.key === "w" || e.key === "W") KW = false;
if (e.key === "s" || e.key === "S") KS = false;
});
document.addEventListener("mousemove", function(e) {
var rect = canvas.getBoundingClientRect();
mouseX = Math.floor((e.clientX - rect.left) / pixelSize);
mouseY = Math.floor((e.clientY - rect.top) / pixelSize);
});
// --- WIZARD BASE ---
elements.wizard = {
color: "#5a00a0",
behavior: behaviors.WALL,
category: "noita",
state: "solid",
hardness: 1.2,
conduct: 0.4,
fireResistance: 25,
burn: 100,
burnTime: 150,
burnInto: "ash",
density: 2000,
desc: "A magical being controlled with wasd. Dies easily.",
tick: function(pixel) {
if (typeof pixel.temp === "undefined") pixel.temp = 30;
if (pixel.temp < 30) pixel.temp += 1;
else if (pixel.temp > 30) pixel.temp -= 1;
if (pixel.temp > 100) {
pixel.hotTicks ??= 0;
pixel.hotTicks++;
if (pixel.hotTicks >= 60) {
changePixel(pixel, "ash");
return;
}
} else {
pixel.hotTicks = 0;
}
if (pixel.hotTicks >= 30 && Math.random() < 0.2) {
createPixel("smoke", pixel.x, pixel.y - 1);
}
let coldFactor = 1;
if (pixel.temp < 0) {
coldFactor = Math.max(0.02, Math.pow((pixel.temp + 100) / 100, 2));
}
if (typeof pixel.spellCooldown === "undefined") pixel.spellCooldown = 0;
if (typeof pixel.wasQPressed === "undefined") pixel.wasQPressed = false;
if (isNaN(pixel.vx)) pixel.vx = 0;
if (isNaN(pixel.vy)) pixel.vy = 0;
if (isNaN(pixel.frame)) pixel.frame = 0;
pixel.frame++;
const moveAccel = 0.2 * coldFactor;
const maxV = 1 * coldFactor;
if (KA) pixel.vx = -moveAccel;
else if (KD) pixel.vx = moveAccel;
else pixel.vx = 0;
if (KW) pixel.vy = -1 * coldFactor;
pixel.vy += 0.05;
if (!KW && pixel.vy < 0) pixel.vy = 0;
pixel.vx = Math.max(-maxV, Math.min(maxV, pixel.vx));
pixel.vy = Math.max(-maxV, Math.min(maxV, pixel.vy));
if (pixel.vx !== 0) tryMove(pixel, pixel.x + Math.sign(pixel.vx), pixel.y);
if (pixel.vy !== 0 && pixel.frame % 2 === 0) tryMove(pixel, pixel.x, pixel.y + Math.sign(pixel.vy));
if (!isEmpty(pixel.x, pixel.y + 1) && pixel.vy > 0) pixel.vy = 0;
if (pixel.spellCooldown > 0) pixel.spellCooldown--;
if (KS && !pixel.wasQPressed && pixel.spellCooldown === 0) {
const spellFns = [
// Magic Missile 1
() => {
let dx = mouseX - pixel.x;
let dy = mouseY - (pixel.y - 2);
let mag = Math.sqrt(dx * dx + dy * dy) || 1;
dx /= mag;
dy /= mag;
let vx = dx * 2;
let vy = dy * 2;
let spawnX = pixel.x;
let spawnY = pixel.y - 2;
createPixel("nfirebolt", spawnX, spawnY);
let bolt = pixelMap[spawnX]?.[spawnY];
if (bolt) {
bolt.vx = vx;
bolt.vy = vy;
}
},
// Swipe 2
() => {
for (let r = 6; r <= 8; r++) {
for (let a = 0; a < 360; a += 10) {
let angle = a * Math.PI / 180;
let x = Math.round(pixel.x + r * Math.cos(angle));
let y = Math.round(pixel.y + r * Math.sin(angle));
if (isEmpty(x, y)) createPixel("cold_fire", x, y);
}
}
},
// Shield 3
() => {
for (let dx = -3; dx <= 3; dx++) {
for (let dy = -3; dy <= 3; dy++) {
if ((Math.abs(dx) === 3 || Math.abs(dy) === 3) && isEmpty(pixel.x + dx, pixel.y + dy)) {
createPixel("nmagicbarrier", pixel.x + dx, pixel.y + dy);
}
}
}
},
// Meteor 4
() => {
if (isEmpty(pixel.x, pixel.y - 10)) {
createPixel("nbomb", pixel.x, pixel.y + 2);
}
},
// Rain 5
() => {
createPixel("rain_call", pixel.x, pixel.y - 6);
},
// Bridge 6
() => {
for (let y = 1; y <= 3; y++) {
if (isEmpty(pixel.x, pixel.y + y)) {
createPixel("dirt", pixel.x, pixel.y + y);
}
}
},
// Wisp 7
() => {
createPixel("nwisp", pixel.x, pixel.y - 2);
},
// Growth Burst 8
() => {
let growthOptions = ["plant", "grass", "seeds", "vine", "flower_seed", "sapling"];
for (let i = 0; i < 5; i++) {
let gx = pixel.x + Math.floor(Math.random() * 5 - 2);
let gy = pixel.y + Math.floor(Math.random() * 5 - 2);
if (isEmpty(gx, gy)) {
let pick = growthOptions[Math.floor(Math.random() * growthOptions.length)];
createPixel(pick, gx, gy);
}
}
},
// Obliteration Blink
() => {
const dashLength = 20;
let dx = mouseX - pixel.x;
let dy = mouseY - pixel.y;
let mag = Math.sqrt(dx * dx + dy * dy) || 1;
dx = Math.round((dx / mag) * dashLength);
dy = Math.round((dy / mag) * dashLength);
let targetX = pixel.x + dx;
let targetY = pixel.y + dy;
// 💣 Step 1: Destroy 4x4 area around the wizard
for (let dx = -2; dx <= 1; dx++) {
for (let dy = -2; dy <= 1; dy++) {
let tx = pixel.x + dx;
let ty = pixel.y + dy;
if (!isEmpty(tx, ty) && pixelMap[tx][ty] !== pixel) {
deletePixel(tx, ty);
}
}
}
// 💨 Step 2: Leave trail of particles
for (let i = 0; i < 5; i++) {
let tx = Math.round(pixel.x + (i / 5) * (targetX - pixel.x));
let ty = Math.round(pixel.y + (i / 5) * (targetY - pixel.y));
if (isEmpty(tx, ty)) {
createPixel("nspell_dust", tx, ty);
}
}
// 🌫️ Puff at old spot
for (let i = 0; i < 4; i++) {
createPixel("smoke", pixel.x + Math.floor(Math.random()*3 - 1), pixel.y + Math.floor(Math.random()*3 - 1));
}
// 🚀 Step 3: Move the wizard
movePixel(pixel, targetX, targetY);
},
// Crystal Bridge 0
() => {
let dx = mouseX - pixel.x;
let dy = mouseY - pixel.y;
let mag = Math.sqrt(dx * dx + dy * dy) || 1;
dx /= mag;
dy /= mag;
for (let i = 1; i <= 7; i++) {
let x = Math.round(pixel.x + dx * i);
let y = Math.round(pixel.y + dy * i);
if (isEmpty(x, y)) {
createPixel("stained_glass", x, y);
}
}
}
];
// ✨ Visual spell cast effect
// ✨ Triangle particle effect
let baseAngle = (pixel.frame || 0) * 0.15; // slowly rotates
let radius = 4;
for (let i = 0; i < 3; i++) {
let angle = baseAngle + i * (2 * Math.PI / 3); // 0, 120°, 240°
let x = Math.round(pixel.x + radius * Math.cos(angle));
let y = Math.round(pixel.y + radius * Math.sin(angle));
if (isEmpty(x, y)) {
createPixel("nspell_dust", x, y);
let p = pixelMap[x]?.[y];
if (p) {
// Set velocity tangent to the circle (spinning effect)
let spinForce = pixel.spinVelocity ?? 2; // starts strong
let tangentAngle = angle + Math.PI / 2;
p.vx = spinForce * Math.cos(tangentAngle);
p.vy = spinForce * Math.sin(tangentAngle);
}
}
}
spellFns[selectedSpell]?.();
pixel.spellCooldown = 5;
}
pixel.wasSPressed = KS;
}
};
// NOITA -------------------------------------------------------
// SPELLS -----------------------------------------------------
// SECTION ---------------------------------------------------
// --- MAGIC BARRIER ---
elements.nmagicbarrier = {
color: "#b8f0ff",
behavior: behaviors.WALL,
category: "noita",
state: "solid",
density: 50,
pushable: 1,
tick: function(pixel) {
if (!pixel || typeof pixel.x !== "number" || typeof pixel.y !== "number") return;
if (Math.random() < 0.02) {
deletePixel(pixel.x, pixel.y);
}
}
};
// --- FIREBOLT ---
elements.nfirebolt = {
color: "#ff7300",
behavior: behaviors.MOVE,
category: "noita",
state: "gas",
tick: function(pixel) {
if (isNaN(pixel.vx)) pixel.vx = 0;
if (isNaN(pixel.vy)) pixel.vy = 0;
// Slight gravity
pixel.vy += 0.02;
// Move
let newX = pixel.x + Math.round(pixel.vx);
let newY = pixel.y + Math.round(pixel.vy);
if (isEmpty(newX, newY)) {
movePixel(pixel, newX, newY);
} else {
// Impact: burst of plasma
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (Math.random() < 0.6 && isEmpty(pixel.x + dx, pixel.y + dy)) {
createPixel("fire", pixel.x + dx, pixel.y + dy);
}
}
}
deletePixel(pixel.x, pixel.y);
}
}
};
// --- BOMB ---
elements.nbomb = {
color: "#333333",
behavior: behaviors.POWDER,
category: "noita",
state: "solid",
desc: "Explodes 2 seconds after being placed.",
cooldown: 90, // tracks ticks
tick: function(pixel) {
if (isNaN(pixel.life)) pixel.life = 0;
pixel.life++;
// Visual cue: start flashing after 40 ticks
if (pixel.life > 40 && Math.random() < 0.3) {
createPixel("electric", pixel.x + (Math.random() < 0.5 ? 1 : -1), pixel.y - 1);
}
if (pixel.life >= 60) {
// Boom! Create fire/plasma/etc
for (let dx = -2; dx <= 2; dx++) {
for (let dy = -2; dy <= 2; dy++) {
if (Math.random() < 0.7 && isEmpty(pixel.x + dx, pixel.y + dy)) {
createPixel(Math.random() < 0.5 ? "fire" : "pop", pixel.x + dx, pixel.y + dy);
}
}
}
deletePixel(pixel.x, pixel.y);
}
}
};
// --- SPELL DUST ---
elements.nspell_dust = {
color: "#00cfff",
behavior: ["XX", "XX", "XX"],
category: "noita",
state: "gas",
density: 1,
temp: 20,
tick: function(pixel) {
if (Math.random() < 0.05) {
deletePixel(pixel.x, pixel.y);
}
}
};
// --- RAIN CALL ---
elements.rain_call = {
color: "#0077ff",
behavior: behaviors.SUPPORT,
category: "noita",
state: "gas",
hidden: true,
tick: function(pixel) {
if (isNaN(pixel.life)) pixel.life = 0;
pixel.life++;
if (pixel.life <= 60) {
// Rain for 2 seconds
if (Math.random() < 0.6) {
let rx = pixel.x + Math.floor(Math.random() * 5 - 2);
if (isEmpty(rx, pixel.y - 1)) {
createPixel("water", rx, pixel.y - 1);
}
}
} else {
deletePixel(pixel.x, pixel.y);
}
}
};