diff --git a/mods/mix.js b/mods/mix.js new file mode 100644 index 00000000..dbcdf57f --- /dev/null +++ b/mods/mix.js @@ -0,0 +1,65 @@ + +// Created by Rain :o 21/11 2024 + +runPerPixel(function(pixel) { // adds a universal tick: function(pixel) to every pixel no matter the element + + if (view != 3) { // don't continue the effect when on basic view + if (elements[pixel.element].state == "liquid") { + find() + let finalColor = averageColor(colorArray) + pixel.color = finalColor + + } + } + + function find() { // finds and stores the colors of viable surrounding pixels + colorArray = []; // empty array to fill later + + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -1; dy <= 1; dy++) { + let x = pixel.x + dx; + let y = pixel.y + dy; + + if (Math.abs(dx) + Math.abs(dy) > 1) continue; // makes it not check diagonal pixels for color averaging for better performance + + let neighboringPixel = pixelMap[x]?.[y]; + + if (neighboringPixel && elements[neighboringPixel.element].state == "liquid") { + colorArray.push(neighboringPixel.color); + } + + } + } + } + + function averageColor(colors) { // calculates the average of colors given by find() + let totalR = 0, totalG = 0, totalB = 0; + + // Loop through each color and get RGB components + colorArray.forEach(color => { + + let match = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + + if (match) { + let r = parseInt(match[1]); + let g = parseInt(match[2]); + let b = parseInt(match[3]); + + // Accumulate RGB values from every color examined + totalR += r; + totalG += g; + totalB += b; + } + }); + + // Divide the total value for each channel by the number of colors to get the average + let count = colorArray.length; + let avgR = Math.round(totalR / count); // Math.round rounds the value to the nearest integer as pixel brightness cannot be in fractions + let avgG = Math.round(totalG / count); + let avgB = Math.round(totalB / count); + + // Return the average color as an RGB string + return `rgb(${avgR}, ${avgG}, ${avgB})`; // formatted to be readable by Sandboxels as a color + } + +}) \ No newline at end of file diff --git a/mods/rainsmod.js b/mods/rainsmod.js new file mode 100644 index 00000000..275ffc65 --- /dev/null +++ b/mods/rainsmod.js @@ -0,0 +1,410 @@ + +// Created by Rain :o 19/11 2024 + +elements.car = { + color: ["#ee70aa", "#ab33ef", "#5e6eee"], + category: "special", + density: 10000, + state: "solid", + tempHigh: 400, + stateHigh: ["steel", "molten_plastic", "glass"], + breakInto: ["steel", "plastic", "glass_shard"], + reactions: { + "water": { elem1: "rust", chance: 0.003 }, + "dirty_water": { elem1: "rust", chance: 0.003 }, + "salt_water": { elem1: "rust", chance: 0.006 }, + "grape": { elem2: "juice", chance: 0.1, color2: "#291824" }, + "tomato": { elem2: "sauce", chance: 0.1 }, + "egg": { elem2: "yolk", chance: 0.1 }, + "malware": {elem1: "explosion"}, + }, + flippableX: true, + tick: function (pixel) { + + if (pixel.carFlip === undefined) { + pixel.carFlip = 1; //it's the "pixel." that gives the variable to each car instance, very important :) + } + tryMove(pixel, pixel.x, pixel.y + 1); //try to move down (fall) + if (!isEmpty(pixel.x, pixel.y + 1)) { //if it didn't work (if the car is on the ground): + + if (isEmpty(pixel.x + pixel.carFlip, pixel.y + 1)) { + tryMove(pixel, pixel.x + pixel.carFlip, pixel.y + 1); // move diagonally down to avoid falling when going downhill + } + else if (isEmpty(pixel.x + pixel.carFlip, pixel.y)) { + tryMove(pixel, pixel.x + pixel.carFlip, pixel.y); //move to the side (which side is derived from current carFlip state) + + } else if (isEmpty(pixel.x + pixel.carFlip, pixel.y - 1)) { + tryMove(pixel, pixel.x + pixel.carFlip, pixel.y - 1); //move diagonally up the hill + + } else { //if no movement possible (like when hitting a wall): + pixel.carFlip = pixel.carFlip * -1; // Update carFlip for this car instance + } + + doDefaults(pixel); + } + }, +}; + +elements.tram = { + color: "#93E493", + conduct: 1, + category: "special", + density: 10000, + state: "solid", + tempHigh: 400, + stateHigh: ["molten_aluminum", "molten_plastic", "glass"], + breakInto: ["aluminum", "plastic", "glass_shard"], + desc: "Powered by electricity. Can hang on conductive materials for suspension railway and can transport people", + reactions: { + "malware": { elem2: "electric" }, + }, + tick: function (pixel) { + + if (pixel.tramFlip === undefined) { + pixel.tramFlip = 1; //tramFlip works like carFlip for the car + } + if (pixel.hasPixel === undefined) { + pixel.hasPixel = 0; + } + + doDefaults(pixel) + + if (pixel.charge > 0) { //only if powered by electricity + if (isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y - 1) && !isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y - 2)) { + var diUpPixel = pixelMap[pixel.x + 1 * pixel.tramFlip][pixel.y - 2] //establishes the variable. Must be down here because it would crash if there is no diUpPixel + if (elements[diUpPixel.element].conduct && diUpPixel.element !== "tram") { //^ is also the reason this is a seperate if statement + tryMove(pixel, pixel.x + 1 * pixel.tramFlip, pixel.y - 1); //move diagonally upwards if there is support + } + else { + flip() + } + } + else if (isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y) && !isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y - 1)) { + var sidePixel = pixelMap[pixel.x + 1 * pixel.tramFlip][pixel.y - 1] + if (elements[sidePixel.element].conduct && sidePixel.element !== "tram") { + tryMove(pixel, pixel.x + 1 * pixel.tramFlip, pixel.y); //move to the side if there is support + } + else { + flip() + } + } + else if (isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y + 1) && !isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y)) { + var diDownPixel = pixelMap[pixel.x + 1 * pixel.tramFlip][pixel.y] + if (elements[diDownPixel.element].conduct && diDownPixel.element !== "tram") { + tryMove(pixel, pixel.x + 1 * pixel.tramFlip, pixel.y + 1); //move diagonally downwards if there is support + } + else { + flip() + } + } + + else if (isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y + 1) && isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y)) { + tryMove(pixel, pixel.x + 1 * pixel.tramFlip, pixel.y + 1); //move diagonally downwards if there isn't support + } + else if (isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y) && isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y - 1)) { + tryMove(pixel, pixel.x + 1 * pixel.tramFlip, pixel.y); //move to the side if there isn't support + } + else if (isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y - 1) && isEmpty(pixel.x + 1 * pixel.tramFlip, pixel.y - 2)) { + tryMove(pixel, pixel.x + 1 * pixel.tramFlip, pixel.y - 1); //move diagonally upwards if there isn't support (uphill) + } + else { + flip() + } + } + + else { //if not powered + if (!isEmpty(pixel.x, pixel.y - 1)) { + let upPixel = pixelMap[pixel.x][pixel.y - 1] //looks at the properties of the pixel above + if (elements[upPixel.element].conduct > 0.1 && upPixel.element !== "tram") { //if the pixel above is conductive but not tram + return //nothing happens ie it doesn't fall + + } + } + tryMove(pixel, pixel.x, pixel.y + 1); //it falls down (same as above) + + } + + function flip() { + pixel.tramFlip = pixel.tramFlip * -1; // changes movement direction + + if (pixel.hasPixel == 1) { + + validSpots = []; + + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -3; dy <= 1; dy++) { + // Calculate the coordinates of the neighboring pixel for each pixel around our pixel + let x = pixel.x + dx; + let y = pixel.y + dy; + + + if (isEmpty(x, y) && isEmpty(x, y + 1)) { + validSpots.push({ x: x, y: y }) + } + } + } + + let randomIndex = Math.floor(Math.random() * validSpots.length); // random number from 0 to to the amount of validSpots + let chosenSpot = validSpots[randomIndex]; + createPixel("human", chosenSpot.x, chosenSpot.y) + pixel.hasPixel = 0 + + + } + else if (pixel.hasPixel == 0) { // else if prevents it picking up a pixel it has just created + validSpots = []; + + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -2; dy <= 2; dy++) { + // Calculate the coordinates of the neighboring pixel + let x = pixel.x + dx; + let y = pixel.y + dy; + + // Access the pixel information from pixelMap + let neighboringPixel = pixelMap[x][y]; + + if (neighboringPixel && neighboringPixel.element == "head") { + + validSpots.push({ x: x, y: y }) + + } + } + } + + if (validSpots.length > 0) { + let randomIndex = Math.floor(Math.random() * validSpots.length); + let chosenSpot = validSpots[randomIndex]; + + deletePixel(chosenSpot.x, chosenSpot.y) + deletePixel(chosenSpot.x, chosenSpot.y + 1) + pixel.hasPixel = 1 + + } + + + } + + } + }, +}; + +elements.bouncy_ball = { + color: "#e35693", + tempHigh: 250, + stateHigh: ["borax", "glue"], + category: "special", + conduct: 1, + density: 1501, + tick: function (pixel) { + if (pixel.fallDist === undefined) { + pixel.fallDist = 0; + } + if (pixel.isFalling === undefined) { + pixel.isFalling = true; + } + + if (pixel.isFalling) { //main loop of a bouncy ball. Functions are defined below + falling() + } else { + rising() + } + if (pixel.charge > 0) { //will bounce on electricity (doesn't work on real bouncy balls :/) + pixel.fallDist = (pixel.fallDist + 1) + rising() + } + doDefaults(pixel); + function falling() { + + if (isEmpty(pixel.x, pixel.y + 1)) { + tryMove(pixel, pixel.x, pixel.y + 1) + pixel.fallDist += 1; //counts how many pixels the ball has fallen so far + } else { //if it touched the ground + pixel.isFalling = false; //will change the outcome of the main if statement and make ball start rising + pixel.fallDist = pixel.fallDist * 3 / 4; //dynamically decreases bounce height based on how high it is, instead of constant 1 per bounce + } + } + function rising() { + if (pixel.fallDist > 0 && isEmpty(pixel.x, pixel.y - 1)) { // because of also checking for an empty pixel above the ball won't get stuck on ceilings + tryMove(pixel, pixel.x, pixel.y - 1) + pixel.fallDist -= 1 + } else { + pixel.isFalling = true; + pixel.fallDist -= 1 //makes the ball lose 1 pixel height each bounce, useful at the end when * 3/4 only results in fractions + } + + } + + } +}; +elements.borax.reactions.slime = { elem1: "bouncy_ball", elem2: null }; + + +elements.microplastic = { + color: ["#adc7c9", "#cadadb", "#6cbda8", "#62d5d4", "#b3b47b"], + behavior: [ + "XX|XX|XX", + "XX|XX|XX", + "M2%25|M1%50|M2%25", + ], + category: "powders", + state: "solid", + density: 700, + tempHigh: 250, + stateHigh: "molten_plastic", + reactions: { + "fish": { elem1: null, elem2: "meat", chance: 0.01 }, + "glue": { elem1: "bead", elem2: null, chance: 0.03 }, + }, +}; +elements.plastic.breakInto = "microplastic"; + +elements.cellulose.reactions.vinegar = { elem1: "bioplastic", elem2: null, tempMin: 40, chance: 0.1 }; + +elements.bioplastic = { + color: "#eee093", + behavior: behaviors.WALL, + category: "solids", + tempHigh: 227, + stateHigh: "molten_bioplastic", + breakInto: "bioplastic_crumbs", + alias: "Cellulose acetate", + desc: "It's biodegradable :)", +}; +elements.bioplastic_crumbs = { + color: ["#dfd499", "#c0e8a0", "#dfab87"], + hidden: true, + behavior: behaviors.POWDER, + category: "powders", + tempHigh: 227, + stateHigh: "molten_bioplastic", + desc: "small pieces of cellulose acetate" +}; + +elements.worm.reactions.bioplastic = { elem2: ["carbon_dioxide", null, null], chance: 0.05, func: behaviors.FEEDPIXEL }; +elements.worm.reactions.bioplastic_crumbs = { elem2: ["carbon_dioxide", null, null], chance: 0.05, func: behaviors.FEEDPIXEL }; +elements.worm.behavior = [ + "SW:dirt,sand,gravel,ash,mycelium,mud,wet_sand,clay_soil,water,salt_water,dirty_water,primordial_soup,blood,infection,color_sand,bioplastic%3|XX|SW:dirt,sand,gravel,ash,mycelium,mud,wet_sand,clay_soil,water,salt_water,dirty_water,primordial_soup,blood,infection,color_sand,bioplastic,bioplastic_crumbs%3", + "M2%10|XX|M2%10", + "SW:dirt,sand,gravel,ash,mycelium,mud,wet_sand,clay_soil,water,salt_water,dirty_water,primordial_soup,blood,infection,color_sand,bioplastic%3|M1|SW:dirt,sand,gravel,ash,mycelium,mud,wet_sand,clay_soil,water,salt_water,dirty_water,primordial_soup,blood,infection,color_sand,bioplastic,bioplastic_crumbs%3", +]; +elements.cell.reactions.bioplastic = { elem2: ["carbon_dioxide", null, null], chance: 0.02, func: behaviors.FEEDPIXEL }; +elements.cell.reactions.bioplastic_crumbs = { elem2: ["carbon_dioxide", null, null], chance: 0.02, func: behaviors.FEEDPIXEL }; + +elements.molten_bioplastic = { + color: "#ccccac", + behavior: behaviors.LIQUID, + viscosity: 300, + category: "states", + state: "liquid", + tempLow: 210, + stateLow: "bioplastic", + temp: 227, + density: 1300, + hidden: true, +}; + + +elements.glycerol = { + color: "#eefcee", + behavior: behaviors.LIQUID, + viscosity: 1412, + category: "liquids", + state: "liquid", + density: 1261, + tempLow: 18, + tempHigh: 290, + reactions: { }, // empty reaction slot to fill it with a reaction later (depending on mods used) + burn: 5, + burnTime: 40, +}; + +elements.nitro.tempHigh = 218; // More accurate detonation temperature +elements.salt_water.tempLow = -20; // Melting point depression + +elements.nitro.tick = function (pixel) { // Exothermic decomposition of nitroglycerin when above 60° + if (pixel.temp > 60) { + pixel.temp += 1; + + if (Math.random() > 0.999) { // 1 in 1000 chance each tick + var possibleElements = ["oxygen", "nitrogen", "nitrogen", "steam", "steam", "steam", "carbon_dioxide", "carbon_dioxide", "carbon_dioxide"]; //array of possibilities for changing the nitroglycerin pixel + + var randomElement = possibleElements[Math.floor(Math.random() * possibleElements.length)]; // Randomly selecting an element from the array + + changePixel(pixel, randomElement); // Change the pixel to the randomly selected element + } + } +} + +if (enabledMods.includes("mods/chem.js")) { + runAfterLoad(function () { + elements.glycerol.reactions.nitric_acid = { elem1: "nitro", elem2: null, chance: 0.05, temp1: 80 }; // Nitric acid nitrates glycerol to make nitroglycerin + elements.nitric_acid.ignore.push("glycerol", "nitro") // required to avoid the acid dissolving the newly created products + + elements.copper.reactions.sulfuric_acid = { elem1: "copper_sulfate", elem2: "sulfur_dioxide", chance: 0.07, tempMin: 200 }; + elements.oxidized_copper.reactions.sulfuric_acid = { elem1: "copper_sulfate", elem2: null, chance: 0.05} + elements.sulfuric_acid.ignore.push("copper_sulfate", "molten_copper_sulfate", "oxidized_copper") + + elements.sodium_hydroxide.ignore.push("grease", "soap", "fat"); + elements.potassium_hydroxide.ignore.push("grease", "soap", "fat"); + + + }); +} else { + elements.glycerol.reactions.acid = { elem1: "nitro", elem2: null, chance: 0.05, temp1: 80 }; // if we don't have nitric acid from chem.js, "acid" is a good approximation + elements.acid.ignore.push("glycerol", "nitro") +} + +elements.red_cabbage = { + color: ["#884466", "#774499"], + behavior: behaviors.POWDER, + category: "food", + isFood: true, + desc: "Can be boiled in water to release its natural pH indicator", + tempHigh: 300, + stateHigh: ["ash", "steam"], + reactions: { + water: { elem2: "ph_indicator", tempMin: 50, chance: 0.06 }, + vinegar: { elem1: "pickle", color1: "#442266", chance: 0.01 }, + broth: { color2: "#8855aa", chance: 0.05}, + }, +} +elements.snail.reactions.red_cabbage = { elem2: null, func: behaviors.FEEDPIXEL, chance: 0.05 } +elements.slug.reactions.red_cabbage = { elem2: null, func: behaviors.FEEDPIXEL, chance: 0.05 } +elements.bird.reactions.red_cabbage = { elem2: null, func: behaviors.FEEDPIXEL, chance: 0.05 } +elements.fish.reactions.red_cabbage = { elem2: null, func: behaviors.FEEDPIXEL, chance: 0.05 } +elements.head.reactions.red_cabbage = { elem2: null, chance: 0.1 } + +elements.ph_indicator = { + color: "#884499", + behavior: behaviors.LIQUID, + category: "liquids", + state: "liquid", + name: " pHIndicator", + density: 1000, + tempHigh: 100, + stateHigh: ["water", "water", "sugar"], + alias: "Anthocyanin", + reactions: { + acid: { color1: "#bb3030" }, + nitric_acid: { color1: "#bb3030" }, //chem.js + sulfuric_acid: { color1: "#bb3030" }, //chem.js + vinegar: { color1: "#bb2255" }, + seltzer: { color1: "#bb2288" }, + soda: { color1: "#bb2288" }, + juice: { color1: "#bb2288" }, + copper_sulfate: { color1: "#bb2288" }, + sauce: { color1: "#aa3399"}, + coffee: { color1: "#aa44aa" }, + salt_water: { color1: "#5544cc" }, + blood: { color1: "#5544cc" }, + baking_soda: { color1: "#2255bb" }, + soap: { color1: "#2288bb" }, + ammonia: { color1: "#228877"}, + bleach: { color1: "#99bb44" }, + caustic_potash: { color1: "#d7d733" }, + sodium_hydroxide: { color1: "#d7d733" }, //chem.js + }, + +} +elements.potassium.reactions.ph_indicator = { elem1: ["caustic_potash"], elem2: ["hydrogen", "pop", "fire"], chance: 0.01, temp2: 400 } +elements.acid.ignore.push("ph_indicator") \ No newline at end of file