From 6b926e53bf37bebf5d3c9a6514f1af7bd0dac8fe Mon Sep 17 00:00:00 2001 From: zeno Date: Wed, 28 Feb 2024 11:59:54 -0500 Subject: [PATCH] init --- CNAME | 1 + README.md | 4 + css/core.css | 44 +++++++++ index.html | 80 +++++++++++++++ js/clone.js | 60 ++++++++++++ js/cohesion.js | 73 ++++++++++++++ js/combine.js | 53 ++++++++++ js/conway.js | 49 ++++++++++ js/core.js | 218 +++++++++++++++++++++++++++++++++++++++++ js/core_blocks.js | 239 +++++++++++++++++++++++++++++++++++++++++++++ js/event.js | 14 +++ js/gravity.js | 99 +++++++++++++++++++ js/loader.js | 55 +++++++++++ js/save_load.js | 113 +++++++++++++++++++++ js/state.js | 27 +++++ js/temperature.js | 49 ++++++++++ js/tick_handler.js | 55 +++++++++++ js/tile.js | 108 ++++++++++++++++++++ json/TestMod.json | 24 +++++ 19 files changed, 1365 insertions(+) create mode 100644 CNAME create mode 100644 README.md create mode 100644 css/core.css create mode 100644 index.html create mode 100644 js/clone.js create mode 100644 js/cohesion.js create mode 100644 js/combine.js create mode 100644 js/conway.js create mode 100644 js/core.js create mode 100644 js/core_blocks.js create mode 100644 js/event.js create mode 100644 js/gravity.js create mode 100644 js/loader.js create mode 100644 js/save_load.js create mode 100644 js/state.js create mode 100644 js/temperature.js create mode 100644 js/tick_handler.js create mode 100644 js/tile.js create mode 100644 json/TestMod.json diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..44d3dac --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +altboxels.qazox.dev \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..36fcd48 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# altboxels +A sandbox game inspired by https://sandboxels.r74n.com/, with a cleaner codebase and secure/simple modding support in mind. + +Some of the elements are derived from other games in the falling sand genre, especially Sandboxels. However, the physics engine and other backend code are custom and independently developed. \ No newline at end of file diff --git a/css/core.css b/css/core.css new file mode 100644 index 0000000..6f0967f --- /dev/null +++ b/css/core.css @@ -0,0 +1,44 @@ +body { + font-family: monospace; + padding: 10px; + margin: 0; + background: rgb(21, 21, 22); + color: white; + text-align: center; +} + +#no-overflow { + overflow: hidden; +} + +canvas { + margin: auto; + display: block; + background: rgb(181, 204, 253); + image-rendering: pixelated; +} + +#main2 { + position: fixed; + top: 0; + left: 0; + width: 100vw !important; + height: 100vh !important; +} + +button, a, textarea { + padding: 5px; + border: none; + margin: 6px 3px 0px 3px; + + display: none; + + background: rgb(44, 142, 255); + color: white; + + font-family: monospace; +} + +section:target button, .menu2 button, a, textarea { + display: inline-block; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3d327c5 --- /dev/null +++ b/index.html @@ -0,0 +1,80 @@ + + + + + + Altboxels + + + + + + + + +

Altboxels

+ + + + + N/A + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/clone.js b/js/clone.js new file mode 100644 index 0000000..eec67f5 --- /dev/null +++ b/js/clone.js @@ -0,0 +1,60 @@ +/* + Code for element combinations. + TOOD: clean this up too +*/ + +function duplicate(event) { + if (event.type != 'tick') return; + + let cx = event.data[0]; + let cy = event.data[1]; + let chunks = event.canvas; + + let maxMass = -1; + let currBlock = chunks.getBlock(cx, cy); + let dir = [0,0]; + + for (let x = -1; x < 2; x++) { + for (let y = 1; y >= -1; y--) { + let blok = chunks.getBlock(cx + x, cy + y); + + let mass = (blok != -1 && blok) ? mainTiles.tiles[blok].attributes.mass : 0; + + if (mass > maxMass && blok != currBlock) { + dir = [x,y]; + maxMass = mass; + } + } + } + + let offBlock = chunks.getBlock(cx + dir[0], cy + dir[1]); + let offTemp = chunks.getBlock(cx + dir[0], cy + dir[1], true); + + if (currBlock == -1 || offBlock == -1 || offBlock == currBlock) return; + + for (let x = -1; x < 2; x++) { + for (let y = 1; y >= -1; y--) { + + let blok = chunks.getBlock(cx + x, cy + y); + let oldTemp = chunks.getBlock(cx + x, cy + y, true); + + if (oldTemp < -300) oldTemp = -300; + + if (blok == currBlock) continue; + chunks.setBlock(cx + x, cy + y, offBlock); + chunks.setBlock(cx + x, cy + y, offTemp + oldTemp * 0.01, true); + } + } + + return true; +} + +Tile.prototype.duplicate = function () { + let that = this; + + that.interactions.push(function (event) { + duplicate(event) + }); + + return this; +} \ No newline at end of file diff --git a/js/cohesion.js b/js/cohesion.js new file mode 100644 index 0000000..70f2d92 --- /dev/null +++ b/js/cohesion.js @@ -0,0 +1,73 @@ +/* + Code for cohesion. + Allows water to stick to itself. + + radius: How far out of blocks will contribute to sticking + isAll: + If this is false or undefined, cohesion occurs (sticks to self) + If this is true, adhesion and cohesion ocucrs (sticks to non-air) + +*/ + +function cohesion(event, radius, isAll = 0) { + if (event.type != 'tick') return; + + let cx = event.data[0]; + let cy = event.data[1]; + let chunks = event.canvas; + + let dir = [0, 0]; + let force = [0,0]; + + let currBlock = chunks.getBlock(cx, cy); + + for (let x = -radius; x <= radius; x ++) { + for (let y = -radius; y <= radius; y++) { + let blok = chunks.getBlock(cx + x, cy + y); + + let factor = ((blok == currBlock) * (1-isAll)) + ((blok != air) * isAll); + + if (factor == 0) continue; + + let dist = 0.1 + (1/8 * (-x * -x - y * y + 8) * (x * x + y * y)); + + force[0] += x / dist * factor; + force[1] += y / dist * factor; + } + } + + if (force[0] == 0 && force[1] == 0) return; + + dir[0] = (Math.abs(force[0]) < radius*0.1) ? 0 : Math.sign(force[0]); + dir[1] = (Math.abs(force[1]) < radius*0.1) ? 0 : Math.sign(force[1]); + + if (Math.abs(force[0]) < Math.abs(force[1])) { + force[0] = 0 + } else { + force[1] = 0 + } + + let offBlock = chunks.getBlock(cx + dir[0], cy + dir[1]); + + if (currBlock == -1 || offBlock == -1 || offBlock != air || currBlock == offBlock || chunks.noTick[(cx+dir[0])*chunks.height + (cy+dir[1])]) return; + + chunks.setBlock(cx, cy, offBlock); + chunks.setBlock(cx + dir[0], cy + dir[1], currBlock); + + let t = chunks.getBlock(cx, cy,true); + let t2 = chunks.getBlock(cx + dir[0], cy + dir[1],true); + + if (t != undefined && t2 != undefined) { + chunks.setBlock(cx, cy,t2, true); + chunks.setBlock(cx + dir[0], cy + dir[1], t, true); + } + + return true; +} + +Tile.prototype.cohesion = function (radius, isAll) { + this.interactions.push(function (event) { + cohesion(event, radius, isAll) + }); + return this; +} \ No newline at end of file diff --git a/js/combine.js b/js/combine.js new file mode 100644 index 0000000..d670706 --- /dev/null +++ b/js/combine.js @@ -0,0 +1,53 @@ +/* + Code for element combinations. + TOOD: clean this up too +*/ + +function combine(event, inBlock, outBlock, outBlock2, mustAir = false) { + if (event.type != 'tick') return; + + let cx = event.data[0]; + let cy = event.data[1]; + let chunks = event.canvas; + + let dir = [0, 0]; + let currBlock = chunks.getBlock(cx, cy); + + for (let x = -1; x < 2; x++) { + for (let y = 1; y >= -1; y--) { + if (chunks.getBlock(cx + x, cy + y) == inBlock) { + dir = [x, y]; + continue; + } + } + } + + let offBlock = chunks.getBlock(cx + dir[0], cy + dir[1]); + let offBlock2 = chunks.getBlock(cx + dir[0], cy + dir[1] - 1); + + if (mustAir && offBlock2 != air) return; + if (currBlock == -1 || offBlock == -1 || offBlock != inBlock || currBlock == offBlock || chunks.noTick[(cx + dir[0]) * chunks.height + (cy + dir[1])]) return; + + chunks.setBlock(cx, cy, outBlock); + chunks.setBlock(cx + dir[0], cy + dir[1], outBlock2); + + return true; +} + +Tile.prototype.combine = function (inBlock, outBlock2, outBlock, mustAir = false) { + let that = this; + + setTimeout(function() { + + inBlock = mainTiles.resolveID(inBlock[0],inBlock[1]); + outBlock = mainTiles.resolveID(outBlock[0],outBlock[1]); + outBlock2 = mainTiles.resolveID(outBlock2[0],outBlock2[1]); + + that.interactions.push(function (event) { + + combine(event, inBlock, outBlock, outBlock2, mustAir) + }); + },200) + + return this; +} \ No newline at end of file diff --git a/js/conway.js b/js/conway.js new file mode 100644 index 0000000..182f253 --- /dev/null +++ b/js/conway.js @@ -0,0 +1,49 @@ +/* + Conway's Game of Life. + +*/ + +let queuedChanges = []; + +function life(event, liveTile, deadTile) { + if (event.type != 'tick') return; + + let cx = event.data[0]; + let cy = event.data[1]; + let chunks = event.canvas; + + let neighbors = 0; + + let currBlock = chunks.getBlock(cx, cy); + + liveTile = mainTiles.resolveID(liveTile[0], liveTile[1]); + deadTile = mainTiles.resolveID(deadTile[0], deadTile[1]); + + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + if (x == 0 && y == 0) continue; + let blok = chunks.getBlock(cx + x, cy + y); + neighbors += (blok == liveTile) ? 1 : 0; + } + } + + if (currBlock == -1 || (currBlock != liveTile && currBlock != deadTile)) return; + + if ((neighbors < 2 || neighbors > 3) && currBlock != deadTile) { + chunks.queuedChanges.push([cx, cy, deadTile]) + } + + if (neighbors == 3 && currBlock != liveTile) { + chunks.queuedChanges.push([cx, cy, liveTile]) + } + + return true; + +} + +Tile.prototype.life = function (liveTile, deadTile) { + this.interactions.push(function (event) { + life(event, liveTile, deadTile) + }); + return this; +} \ No newline at end of file diff --git a/js/core.js b/js/core.js new file mode 100644 index 0000000..62b964e --- /dev/null +++ b/js/core.js @@ -0,0 +1,218 @@ +/* + Code for rendering and startup. +*/ + +function Canvas(width, height, upscale) { + this.width = width; + this.height = height; + this.upscale = upscale; + this.radius = 2; + + this.x = 0; + this.y = 0; + + this.elem = document.querySelector('canvas'); + this.ctx = this.elem.getContext('2d'); + + this.blocks = new Uint16Array(width * height); + this.temp = new Array(width * height); // This will not be saved in the world data. + + for (let i = 0; i < width * height; i++) { + this.temp[i] = 0; + } + + this.sel = -1; + + this.queuedChanges = []; + + let that = this; + + this.elem.addEventListener('mousedown', function (e) { + that.clicked = true; + }); + + this.elem.addEventListener('mousemove', function (e) { + that.pageX = e.pageX; + that.pageY = e.pageY; + }) + + this.elem.addEventListener('wheel', function (e) { + that.radius += Math.sign(e.deltaY); + if (that.radius < 0) that.radius = 0; + }) + + this.elem.addEventListener('mouseup', function (e) { + that.clicked = false; + + that.firstX = this.pageX; + that.firstY = this.pageY; + }); + + + this.clicked = false; + this.pageX = 0; + this.pageY = 0; + + this.resize(); +} + +Canvas.prototype.getBlock = function (x, y, doTemp) { + if (x < 0 || y < 0 || x >= this.width || y >= this.height) { + if (doTemp) return undefined; + return -1; + } + return (doTemp ? this.temp : this.blocks)[x * this.height + y]; +} + +Canvas.prototype.setBlock = function (x, y, block, doTemp) { + if (this.getBlock(x, y) == -1) return; + (doTemp ? this.temp : this.blocks)[x * this.height + y] = block +} + +Canvas.prototype.resize = function () { + this.elem.style.width = this.width * this.upscale + 'px'; + this.elem.style.height = this.height * this.upscale + 'px'; + + this.elem.width = this.width; + this.elem.height = this.height; + + this.render(); +} + +Canvas.prototype.render = function () { + this.ctx.clearRect(0, 0, this.width * this.upscale, this.height * this.upscale); + + let imgData = this.ctx.getImageData(0, 0, this.width, this.height); + let pixels = imgData.data; + + let int = Math.ceil(this.width * this.height/8); + for (let j = 0; j < 8; j++) { + let that = this; + (async function() { + for (let i = j*int; i < (j+1)*int; i++) { + if (i > that.width * that.height) break; + let x = Math.floor(i / that.height); + let y = i % that.height; + + let i2 = x + y*that.width; + + let block = mainTiles.tiles[that.blocks[i]]; + + let temp = that.temp[i]; + + if (block.color[0] != -1) { + + let val = (temp + 310)/310; + if (val < -2.861) val = -2.861; + + pixels[i2*4] = (block.color[0] - temp / 1e28) * val ; + pixels[i2*4+1] = (block.color[1] - temp / 1e28) * (val * 0.259 + 0.741); + pixels[i2*4+2] = (block.color[2] - temp / 1e28) * (val * 0.023 + 0.977); + pixels[i2*4+3] = block.color[3] * 255 + Math.abs(val-1) * 100 || 255; + } else { + let lg = Math.log(temp); + pixels[i2*4] = ((handler.ticks*69 ) % (lg*0.6969)) * 255 / (lg*0.6969); + pixels[i2*4+1] = ((handler.ticks*69) % (lg*0.420420)) * 255 /(lg*0.420420); + pixels[i2*4+2] = ((handler.ticks*69) % (lg*0.13371337)) * 255 / (lg*0.13371337); + pixels[i2*4+3] = 255; + } + } + })() + } + + /* TODO: clean up */ + + this.ctx.putImageData(imgData,0,0,0,0,this.width,this.height) + + if (window.loc2 && loc2.get('only') == 'true') { + this.stopNow = true; + document.querySelector('canvas').id = 'main2'; + document.querySelector('body').id = 'no-overflow'; + return; + } + + let x = (this.pageX - this.elem.getBoundingClientRect().x - scrollX + this.x) - 0.5 - this.radius * this.upscale; + let y = (this.pageY - this.elem.getBoundingClientRect().y - scrollY + this.y) - 0.5 - this.radius * this.upscale; + + + this.ctx.globalAlpha = 1; + + this.ctx.strokeStyle = 'rgb(255,255,255)'; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(x / this.upscale, y / this.upscale, this.radius * 2 + 2, this.radius * 2 + 2); + + let theX = Math.floor(x/this.upscale + this.radius + 1); + let theY = Math.floor(y/this.upscale + this.radius + 1); + + + + let blok = mainTiles.tiles[this.getBlock(theX, theY)]; + let temp = this.getBlock(theX,theY, true); + + if (blok) { + document.querySelector('.info').textContent = `${blok.namespace}; ${blok.id}; ${Math.round(temp+23)}deg Celsius` + } else { + document.querySelector('.info').textContent = `Unknown` + } + +} + +/* TODO: cleanup again */ +Canvas.prototype.click = function () { + if (this.firstX == undefined) { + this.firstX = this.pageX; + this.firstY = this.pageY; + return; + } + + let x = (this.pageX - this.elem.getBoundingClientRect().x - scrollX + this.x) / this.upscale; + let y = (this.pageY - this.elem.getBoundingClientRect().y - scrollY + this.y) / this.upscale; + + let x2 = (this.firstX - this.elem.getBoundingClientRect().x - scrollX + this.x) / this.upscale; + let y2 = (this.firstY - this.elem.getBoundingClientRect().y - scrollY + this.y) / this.upscale; + + x = Math.floor(x); + y = Math.floor(y); + + let x3 = x2 = Math.floor(x2); + let y3 = y2 = Math.floor(y2); + + do { + if (Math.abs(x3 - x) > Math.abs(y3 - y)) { + x3 += Math.sign(x - x3) + } else { + y3 += Math.sign(y - y3) + } + + for (let x4 = x3 - this.radius; x4 <= x3 + this.radius; x4++) { + for (let y4 = y3 - this.radius; y4 <= y3 + this.radius; y4++) { + + let blox = this.getBlock(x4, y4); + + if (blox == -1) continue; + + this.setBlock(x4, y4, mainTiles.tiles[mainTiles.sel].attributes.temperature, true); + this.setBlock(x4, y4, mainTiles.sel); + } + } + + } while (x3 != x && y3 != y) + + this.firstX = this.pageX; + this.firstY = this.pageY; +} + +var canvas = new Canvas(240, 135, 4); +var handler = new TickHandler(canvas); + +setInterval(() => { + if (canvas.stopNow) return; + handler.tick(); +}, 1000 / 60); + + +setInterval(() => { + if (canvas.stopNow) return; + if (canvas.clicked) canvas.click(); + this.canvas.render(); +}, 1000 / 60); \ No newline at end of file diff --git a/js/core_blocks.js b/js/core_blocks.js new file mode 100644 index 0000000..eda97ce --- /dev/null +++ b/js/core_blocks.js @@ -0,0 +1,239 @@ +/* + Code for most implemented blocks. + + This code isn't in JSON form for reading purposes. + + If you want to add a modification, use the JSON format + documented in [js/loader.js]. +*/ + +mainTiles.loadSet( + 'Vanilla/Air', + [ + new Tile('none', 'Air').gravity(1.4 / 1000, 4, 200) + .temperature(0,0.01) + .state(['Vanilla/Air', 'Hot Air'],50,true), + + new Tile('none', 'Hot Air').gravity(1.4 / 1100, 4, 100) + .temperature(100,0.02) + .state(['Vanilla/Air', 'Air'],50,false) + .state(['Vanilla/Air', 'Plasma'],3000,true), + + new Tile('rgb(0,0,0)', 'Vacuum').gravity(0.01 / 1000 / 1000, 4, Infinity) + .temperature(-269.15,0), + + new Tile('rgb(180,156,229)', 'Hydrogen').gravity(0.8 / 1000, 4, 200) //H2 + .temperature(0,0.1) + .state(['Vanilla/Air', 'Plasma'],3000,true) + .combine(['Vanilla/Air', 'Air'], ['Vanilla/Air', 'Hydrogen Flame'], ['Vanilla/Air', 'Hydrogen Flame']) + .combine(['Vanilla/Air', 'Plasma'], ['Vanilla/Air', 'Helium'], ['Vanilla/Air', 'Vacuum']), + + new Tile('rgb(255,0,239)', 'Plasma').gravity(1.4 / 5000, 4, 200) + .temperature(3010,2) + .state(['Vanilla/Air', 'Hydrogen'],3000,false) + .state(['Vanilla/Air', '???'],1e30,true), + + new Tile('random', '???').gravity(1e100, 4, 1e105) + .temperature(1e31,2), + + new Tile('rgb(200,186,249)', 'Hydrogen Flame').gravity(0.8 / 1000, 4, 200) + .temperature(50,0.2) + .state(['Vanilla/Air', 'Plasma'],3000,true) + .combine(['Vanilla/Air', 'Hydrogen'], ['Vanilla/Water', 'Steam'], ['Vanilla/Air', 'Hydrogen']), + + new Tile('rgb(229,194,156)', 'Helium').gravity(0.7 / 1000, 4, 200) + .temperature(0,0.001) + .state(['Vanilla/Air', 'Plasma'],3000,true), + + new Tile('rgba(0,0,0,0.2)', 'Carbon Dioxide').gravity(1.3 / 1000, 4, 200) + .temperature(0,0.03) + .state(['Vanilla/Air', 'Plasma'],3000,true) + .combine(['Vanilla/Fire', 'Fire'], ['Vanilla/Air', 'Carbon Dioxide'], ['Vanilla/Fire', 'Fire']), + + new Tile('rgba(0,0,0,0.4)', 'Methane').gravity(1.2 / 1000, 4, 200) + .temperature(0,0.04) + .state(['Vanilla/Air', 'Plasma'],3000,true) + .combine(['Vanilla/Fire', 'Fire'], ['Vanilla/Air', 'Methane'], ['Vanilla/Fire', 'Fire']) + + ] +); + + +mainTiles.loadSet( + 'Vanilla/Earth', + [ + new Tile('rgb(153, 102, 51)', 'Earth').gravity(10, 1, 91), + + new Tile('rgb(143, 92, 41)', 'Soil').gravity(10, 1, 93), + + new Tile('rgb(255,0,0)', 'Barrier').unGravity(), + + new Tile('rgb(20,20,20)', 'Charcoal').gravity(11, 1, 100) + .temperature(0,0.5) + .combine(['Vanilla/Air', 'Hot Air'], ['Vanilla/Fire', 'Fire'], ['Vanilla/Fire', 'Fire']) + .combine(['Vanilla/Fire', 'Fire'], ['Vanilla/Air', 'Carbon Dioxide'], ['Vanilla/Air', 'Vacuum']) + .combine(['Vanilla/Water', 'Steam'], ['Vanilla/Air', 'Methane'], ['Vanilla/Air', 'Air']), + + new Tile('rgb(53,46,32)', 'Mud').cohesion(2).gravity(12, 1.5, 113), + + new Tile('rgb(43, 33, 42)', 'Mudstone').gravity(10, 1, 89) + .combine(['Vanilla/Water', 'Water'], ['Vanilla/Earth', 'Mud'], ['Vanilla/Air', 'Air']), + + new Tile('rgb(252,224,133)', 'Sand').gravity(10, 1, 93) + .state(['Vanilla/Earth', 'Glass'],1650,true) + .combine(['Vanilla/Fire', 'Fire'], ['Vanilla/Air', 'Air'], ['Vanilla/Earth', 'Sand']) + .combine(['Vanilla/Water', 'Acid'], ['Vanilla/Water', 'Water'], ['Vanilla/Earth', 'Clay']), + + new Tile('rgb(117,111,86)', 'Wet Sand').cohesion(2).gravity(12, 1.5, 112) + .combine(['Vanilla/Fire', 'Fire'], ['Vanilla/Air', 'Air'], ['Vanilla/Earth', 'Packed Sand']), + + new Tile('rgb(187,158,110)', 'Packed Sand').gravity(11, 1.5, 103) + .combine(['Vanilla/Fire', 'Fire'], ['Vanilla/Air', 'Air'], ['Vanilla/Earth', 'Sandstone']), + + new Tile('rgb(167,138,90)', 'Sandstone').unGravity(), + + new Tile('rgba(128,148,168,0.8)', 'Glass').gravity(10, 1, 93), + + new Tile('rgb(128,128,128)', 'Gravel').gravity(10, 1, 932) + .combine(['Vanilla/Water', 'Acid'], ['Vanilla/Water', 'Water'], ['Vanilla/Earth', 'Sand']) + .combine(['Vanilla/Life', 'Mycelium'], ['Vanilla/Air', 'Air'], ['Vanilla/Earth', 'Earth']), + + new Tile('rgb(56, 54, 52)', 'Basalt') + .unGravity() + .temperature(0,0.005) + .state(['Vanilla/Fire', 'Lava'],1000,true), + + new Tile('rgb(169,179,210)', 'Clay').gravity(10, 1, 89) + .state(['Vanilla/Earth', 'Brick'],1000,true), + + new Tile('rgb(211,108,108)', 'Brick').unGravity(), + + new Tile('rgb(66, 64, 62)', 'Rock').gravity(10, 1, 89) + .combine(['Vanilla/Water', 'Acid'], ['Vanilla/Water', 'Water'], ['Vanilla/Earth', 'Gravel']), + + new Tile('rgb(56, 54, 52)', 'Rock Barrier').unGravity() + .state(['Vanilla/Earth', 'Rock'],800,true) + .combine(['Vanilla/Water', 'Water'], ['Vanilla/Earth', 'Rock'], ['Vanilla/Water', 'Water']) + .combine(['Vanilla/Water', 'Acid'], ['Vanilla/Water', 'Water'], ['Vanilla/Earth', 'Rock']), + + new Tile('rgb(235, 235, 235)', 'Salt') + .gravity(10, 1, 110) + .state(['Vanilla/Air', 'Plasma'],3000,true) + ] +) + +mainTiles.loadSet( + 'Vanilla/Life', + [ + new Tile('rgb(114, 204, 123)', 'Grass').gravity(10, 1, 91) + .temperature(0,0.3) + .combine(['Vanilla/Earth', 'Earth'], ['Vanilla/Life', 'Grass'], ['Vanilla/Life', 'Grass'], true) + .state(['Vanilla/Earth', 'Charcoal'],500,true), + + new Tile('rgb(97, 92, 97)', 'Mycelium').gravity(10, 1, 91) + .temperature(0,0.3) + .combine(['Vanilla/Earth', 'Earth'], ['Vanilla/Life', 'Mycelium'], ['Vanilla/Life', 'Mycelium'], true) + .combine(['Vanilla/Earth', 'Earth'], ['Vanilla/Earth', 'Soil'], ['Vanilla/Life', 'Mycelium']) + .state(['Vanilla/Earth', 'Charcoal'],500,true), + + new Tile('rgb(245,245,245)', 'Alive Conway Cell').life( + ['Vanilla/Life', 'Alive Conway Cell'], + ['Vanilla/Life', 'Dead Conway Cell']), + + new Tile('rgb(10,10,10)', 'Dead Conway Cell').life( + ['Vanilla/Life', 'Alive Conway Cell'], + ['Vanilla/Life', 'Dead Conway Cell']), + + new Tile('rgb(25,30,35)', 'Conway Buffer').gravity(10, 1, 91) + .combine(['Vanilla/Life', 'Alive Conway Cell'], ['Vanilla/Life', 'Conway Buffer'], ['Vanilla/Life', 'Grass']) + .combine(['Vanilla/Life', 'Dead Conway Cell'], ['Vanilla/Life', 'Conway Buffer'], ['Vanilla/Life', 'Mycelium']) + ] +); + +mainTiles.loadSet( + 'Vanilla/Water', + [ + + new Tile('rgb(51, 153, 255)', 'Water').cohesion(2,0.2).gravity(1, 2, 110) + .temperature(-5,0.05,5) + .state(['Vanilla/Water', 'Steam'],100,true) + .state(['Vanilla/Water', 'Ice'],-23,false) + .combine(['Vanilla/Earth', 'Earth'], ['Vanilla/Earth', 'Mud'], ['Vanilla/Air', 'Air']) + .combine(['Vanilla/Earth', 'Sand'], ['Vanilla/Earth', 'Wet Sand'], ['Vanilla/Air', 'Air']), + + new Tile('rgb(45, 255, 15)', 'Acid').cohesion(2,0.3).gravity(1.01, 2.2, 110) + .temperature(-5,0.05,5) + .state(['Vanilla/Water', 'Steam'],100,true) + .state(['Vanilla/Water', 'Ice'],-23,false), + + new Tile('rgb(81, 200, 255)', 'Ice').unGravity() + .temperature(-30,0.05,5) + .state(['Vanilla/Water', 'Water'],-23,true), + + new Tile('rgb(255, 255, 255)', 'Snow') + .temperature(-30,0.05,5) + .state(['Vanilla/Water', 'Water'],-23,true) + .gravity(10, 1, 93), + + new Tile('rgb(208,232,237)', 'Steam').gravity(1.2 / 1000, 3, 11) + .temperature(80,0.1,0.5) + .state(['Vanilla/Air', 'Plasma'],3000,true) + .state(['Vanilla/Water', 'Water'],77,false) + .state(['Vanilla/Water', 'Snow'],-23,false), + + new Tile('rgb(145,201,152)', 'Slime').cohesion(5,0.8).gravity(1.5, 2, 16) + .combine(['Vanilla/Water', 'Acid'], ['Vanilla/Earth', 'Salt'], ['Vanilla/Water', 'Water']) + .state(['Vanilla/Water', 'Steam'],100,true) + .state(['Vanilla/Water', 'Ice'],-23,false) + ] +) + +mainTiles.loadSet( + 'Vanilla/Fire', + [ + new Tile('rgb(255, 64, 0)', 'Fire') + .temperature(1200,1) + .state(['Vanilla/Air', 'Carbon Dioxide'],500,false) + .state(['Vanilla/Air', 'Plasma'],3000,true) + .gravity(1.4 / 1000, 4, 200) + .combine(['Vanilla/Water', 'Water'], ['Vanilla/Water', 'Steam'], ['Vanilla/Air', 'Carbon Dioxide']) + .combine(['Vanilla/Air', 'Vacuum'], ['Vanilla/Air', 'Carbon Dioxide'], ['Vanilla/Air', 'Vacuum']) + .combine(['Vanilla/Air', 'Air'], ['Vanilla/Fire', 'Fire'], ['Vanilla/Air', 'Hot Air']) + .combine(['Vanilla/Air', 'Carbon Dioxide'],['Vanilla/Air', 'Carbon Dioxide'],['Vanilla/Air', 'Carbon Dioxide'],), + + new Tile('rgb(128, 32, 0)', 'Lava').cohesion(2,0.2).gravity(1, 2, 11) + .temperature(1125,0.1) + .state(['Vanilla/Earth', 'Basalt'],1000,false) + .combine(['Vanilla/Water', 'Water'], ['Vanilla/Water', 'Steam'], ['Vanilla/Earth', 'Basalt']) + ] +) + +mainTiles.loadSet( + 'Vanilla/Machines', + [ + new Tile('rgb(237, 162, 71)', 'Copper') + .temperature(0,0.1), + + new Tile('rgb(255,255,128)', 'Duplicator') + .unGravity() + .gravity(1000,0,0) + .duplicate() + ] +) + +mainTiles.loadSet( + 'Vanilla/Sponge', + [ + new Tile('rgb(158,150,91)', 'Sponge').unGravity() + .combine(['Vanilla/Water', 'Water'], ['Vanilla/Air', 'Air'], ['Vanilla/Sponge', 'Wet Sponge']), + + new Tile('rgb(86,81,39)', 'Wet Sponge').unGravity() + .combine(['Vanilla/Fire', 'Fire'], ['Vanilla/Water', 'Steam'], ['Vanilla/Sponge', 'Sponge']) + .combine(['Vanilla/Sponge', 'Sponge'], ['Vanilla/Sponge', 'Wet Sponge'], ['Vanilla/Sponge', 'Sponge']), + + new Tile('rgb(255,255,0)', 'Infinite Sponge').unGravity() + .combine(['Vanilla/Water', 'Water'], ['Vanilla/Air', 'Air'], ['Vanilla/Sponge', 'Infinite Sponge']), + ] +); + +let air = mainTiles.resolveID('Vanilla/Air','Air'); diff --git a/js/event.js b/js/event.js new file mode 100644 index 0000000..17b8167 --- /dev/null +++ b/js/event.js @@ -0,0 +1,14 @@ +/* + Code for handling events. +*/ + +function GameEvent(type, target, data, canvas) { + this.type = type; + this.data = data; + this.canvas = canvas; + this.target = target; + + for (let interaction of target.interactions) { + if (interaction(this)) return this; + } +} \ No newline at end of file diff --git a/js/gravity.js b/js/gravity.js new file mode 100644 index 0000000..23486b9 --- /dev/null +++ b/js/gravity.js @@ -0,0 +1,99 @@ +/* + Code for gravity. + + mass: Density (in kg/cm^3) + fluid: Spread of substance + saturation: Maximum mass for gravity reactions to occur +*/ + +function gravity(event, mass, fluid, saturation) { + if (event.type != 'tick') return; + + let cx = event.data[0]; + let cy = event.data[1]; + let chunks = event.canvas; + + let dir = [0, 0]; + let force = [0,0]; + let density = 0; + + let currBlock = chunks.getBlock(cx, cy); + + + for (let x = -1; x < 2; x ++) { + for (let y = -1; y < 2; y++) { + let blok = chunks.getBlock(cx + x, cy + y); + + if (blok == -1) continue; + + let mass2 = mainTiles.tiles[blok].attributes.mass; + + density += mass2; + if (density > saturation) break; + + if (blok == currBlock) continue; + + let massDiff = (mass / mass2) - (mass2 / mass); + let x2 = x / fluid; + let dirDiff = (y - 1 + fluid) / (1/8 * (x2 * x2 - y * y + 8) * (x2 * x2 + y * y)); + + + if (y == 0 && x == 0) dirDiff = 0; + if (isNaN(dirDiff)) dirDiff = 0; + + force[0] += massDiff * x2; + force[1] += massDiff * dirDiff * y; + } + if (density > saturation) break; + } + + dir[0] = (Math.abs(force[0]) < .5) ? 0 : Math.sign(force[0]); + dir[1] = (Math.abs(force[1]) < .5) ? 0 : Math.sign(force[1]); + + if (density > saturation ) { + return; + } + + let offBlock = chunks.getBlock(cx + dir[0], cy + dir[1]); + + if (currBlock == offBlock && density <= saturation) { + dir[0] = Math.sign(force[0]); + offBlock = chunks.getBlock(cx + dir[0], cy + dir[1]); + } + + if (currBlock == -1 || offBlock == -1 || currBlock == offBlock ||offBlock == undefined || mainTiles.tiles[offBlock].attributes.saturation / 9 < mass || chunks.noTick[(cx+dir[0])*chunks.height + (cy+dir[1])]) return; + + + + if (!canGravity[offBlock]) return; + + chunks.noTick[cx*chunks.height + cy] = true; + chunks.noTick[(cx+dir[0])*chunks.height + (cy+dir[1])] = true; + + chunks.setBlock(cx, cy, offBlock); + chunks.setBlock(cx + dir[0], cy + dir[1], currBlock); + + let t = chunks.getBlock(cx, cy,true); + let t2 = chunks.getBlock(cx + dir[0], cy + dir[1],true); + + if (t != undefined && t2 != undefined) { + chunks.setBlock(cx, cy,t2, true); + chunks.setBlock(cx + dir[0], cy + dir[1], t, true); + } + + return true; +} + +Tile.prototype.gravity = function (mass, fluid, saturation) { + this.interactions.push(function (event) { + gravity(event, mass, fluid, saturation) + }); + this.attributes.mass = mass; + this.attributes.saturation = saturation; + return this; +} + +Tile.prototype.unGravity = function () { + this.attributes.noGravity = true; + return this; +} \ No newline at end of file diff --git a/js/loader.js b/js/loader.js new file mode 100644 index 0000000..bf0c679 --- /dev/null +++ b/js/loader.js @@ -0,0 +1,55 @@ +/* + Secure JSON loader for modding purposes. + + Unless you want to do something really fancy + or provide a basis for more mods, + do not use raw JavaScript. + + Instead, use a JSON file to contain your mod's + content, and ask me for essential features to be + added. + + This isn't finished entirely, but should give a + decent basis for modding in the future. +*/ + +legalFuncs = [ + "gravity", + "cohesion", + "combine", + "unGravity", + "life", + "temperature", + "state", + "duplicate" +] + +function loadTiles(stuff) { + for (item in stuff) { + let params = stuff[item].params; + stuff[item] = new Tile(stuff[item].color, stuff[item].name); + for (let param of params) { + if (legalFuncs.indexOf(param.func) == -1) { + console.warn('This function is not supported!'); + continue; + } + stuff[item] = stuff[item][param.func](...param.options) + } + } + return stuff; +} + +function loadMod(stuff) { + for (let thing of stuff) { + mainTiles.loadSet( + thing.namespace, + loadTiles(thing.content) + ) + } +} + +async function openMods(stuff) { + // TODO: don't use prompt + let url = prompt('Type in the URL to the JSON of the mod you want to load.'); + loadMod(await (await fetch(url)).json()) +} diff --git a/js/save_load.js b/js/save_load.js new file mode 100644 index 0000000..00a2c48 --- /dev/null +++ b/js/save_load.js @@ -0,0 +1,113 @@ +/* + Code for saving and loading data. + + Features a somewhat efficient compression algorithm. +*/ + +function save() { + let jason = { + 'pal': [], + 'data': [], + 'width': canvas.width, + 'height': canvas.height + }; + + for (let item of mainTiles.tiles) { + jason.pal.push([ + item.namespace, + item.id + ]) + } + + let json = jason.data; + + for (let i = 0; i < canvas.blocks.length; i += 128) { + let arr = canvas.blocks.slice(i, i + 128); + + let pal = Object.values(arr.filter((v, i, a) => a.findIndex(v2 => (v2 === v)) === i).sort()); + let otherArray; + if (pal.length < 9 && pal.length > 1) { + otherArray = new Uint8Array(64); + + for (let i in otherArray) { + otherArray[i] = (((pal.indexOf(arr[i*2]) * 8) + pal.indexOf(arr[i*2+1])) + 'A'.charCodeAt()) % 128; + } + + } else if (pal.length > 8) { + otherArray = new Uint8Array(128); + + for (let i in otherArray) { + otherArray[i] = (pal.indexOf(arr[i]) + 'A'.charCodeAt()) % 128; + } + + } + + + json[i / 128] = { + 'pal': pal, + 'dat': otherArray ? new TextDecoder('ascii').decode(otherArray) : undefined + }; + } + + document.querySelector('#code').value = JSON.stringify(jason); +} + +function load() { + let jason = JSON.parse(document.querySelector('#code').value); + + let json = jason.data; + + canvas.width = jason.width; + canvas.height = jason.height; + canvas.resize(); + + let mainPal = jason.pal.map(x => mainTiles.resolveID(x[0],x[1])); + + console.log(mainPal); + + for (let i in json) { + let data = json[i]; + let pal = data.pal; + let dat = new TextEncoder('ascii').encode(data.dat); + + let otherArray = new Uint16Array(128); + + if (pal.length < 2) { + for (let i in otherArray) { + otherArray[i] = mainPal[(pal[0])]; + } + + } else if (pal.length < 9) { + for (let i in dat) { + otherArray[i*2] = mainPal[pal[((dat[i] - 'A'.charCodeAt()) & 0x38) / 8]]; + otherArray[i*2+1] = mainPal[pal[(dat[i] - 'A'.charCodeAt()) & 0x7]]; + } + + } else { + for (let i in dat) { + otherArray[i] = mainPal[pal[(dat[i] - 'A'.charCodeAt()) & 0x7F]]; + } + } + + canvas.blocks.set(otherArray,Math.min(i*128,canvas.blocks.length - 128)); + } + + for (let i in canvas.temp) { + canvas.temp[i] = mainTiles.tiles[canvas.blocks[i]].attributes.temperature; + } +} + +var loc3 = new URL(window.location).searchParams; +let loc = loc3.get("embed"); +if (loc3.get('oops') == 'true') { + alert('Oh no!'); +} +var loc2; + +if (loc) { + (async function() { + document.querySelector('#code').value = await fetch(loc).then(x => x.text()); + load(); + loc2 = loc3; + })() +} \ No newline at end of file diff --git a/js/state.js b/js/state.js new file mode 100644 index 0000000..d266a3d --- /dev/null +++ b/js/state.js @@ -0,0 +1,27 @@ +/* + Controls a block's state of matter. +*/ + +function state(event, state, temperature, isMin) { + if (event.type != 'tick') return; + + let cx = event.data[0]; + let cy = event.data[1]; + let chunks = event.canvas; + + let temp = chunks.getBlock(cx, cy, true); + + if (chunks.noTick[cx*chunks.height + cy]) return; + + if ((temp > temperature) == isMin) { + chunks.setBlock(cx, cy, mainTiles.resolveID(state[0],state[1])); + return true; + } +} + +Tile.prototype.state = function ( state2, temperature, isMin) { + this.interactions.push(function (event) { + state(event, state2, temperature, isMin) + }); + return this; +} \ No newline at end of file diff --git a/js/temperature.js b/js/temperature.js new file mode 100644 index 0000000..34ef0e6 --- /dev/null +++ b/js/temperature.js @@ -0,0 +1,49 @@ +/* + Controls a block's temperature, + and how much it will conduct or insulate. + + Temperature is in celsius, + but offset from room temperature (23 celsius). +*/ + + +function temperature(event, conduct, transfer = 0) { + if (event.type != 'temp') return; + + let cx = event.data[0]; + let cy = event.data[1]; + let chunks = event.canvas; + + for (let x = -1; x < 2; x ++) { + for (let y = -1; y < 2; y++) { + let blok = chunks.getBlock(cx + x, cy + y); + let temp = chunks.getBlock(cx, cy, true); + let temp2 = chunks.getBlock(cx + x, cy + y, true); + + if (temp2 == undefined || temp == undefined || (x == 0 && y == 0)) { + if (temp != undefined) chunks.setBlock(cx, cy, Math.max(temp,-296.15), true); + continue; + }; + + let conduct2 = (blok != -1) ? (mainTiles.tiles[blok].attributes.conduct || 0) : 0; + + let conductSum = Math.min(conduct * conduct2 * 10, 0.5); + let s = conductSum*(temp2 - temp + transfer); + + s += temp * -0.01; + if (temp < -296.15 && temp2 > -296.15) s = temp2 - temp; + + chunks.setBlock(cx, cy, Math.max(s + temp,-296.15), true); + chunks.setBlock(cx+x, cy+y, Math.max(temp2 - s,-296.15), true); + } + } +} + +Tile.prototype.temperature = function (temp, conduct, transfer) { + this.interactions.push(function (event) { + temperature(event, conduct, transfer) + }); + this.attributes.temperature = temp; + this.attributes.conduct = conduct; + return this; +} \ No newline at end of file diff --git a/js/tick_handler.js b/js/tick_handler.js new file mode 100644 index 0000000..8a02e9e --- /dev/null +++ b/js/tick_handler.js @@ -0,0 +1,55 @@ +/* + Code for global game ticks. +*/ + +function TickHandler(canvas) { + this.canvas = canvas; + this.ticks = 0; + this.noTick = false; +} + +TickHandler.prototype.tick = function () { + if (this.noTick) return; + + let canvas = this.canvas; + + this.canvas.noTick = new Uint16Array(canvas.blocks.length); + + for (let i = 0; i < canvas.width * canvas.height; i++) { + let cx = Math.floor(i / canvas.height); + let cy = i % canvas.height; + + if (this.canvas.noTick[i]) continue; + + let currBlock = this.canvas.blocks[i]; + + let allEq = true; + + if (this.ticks % 10 == 0) { + new GameEvent('temp', mainTiles.tiles[currBlock], [cx, cy, this.ticks], this.canvas); + } + + for (let x = -1; x < 2; x ++) { + for (let y = -1; y < 2; y++) { + let blok = this.canvas.getBlock(cx + x, cy + y); + + allEq = (blok == currBlock); + + if (!allEq) break; + } + if (!allEq) break; + } + + if (allEq) continue; + + new GameEvent('tick', mainTiles.tiles[currBlock], [cx, cy, this.ticks], this.canvas); + } + + for (let change of canvas.queuedChanges) { + canvas.setBlock(change[0], change[1], change[2]); + } + canvas.queuedChanges = []; + + this.ticks++; + this.ticks = this.ticks % 3600; +} diff --git a/js/tile.js b/js/tile.js new file mode 100644 index 0000000..c8eed45 --- /dev/null +++ b/js/tile.js @@ -0,0 +1,108 @@ +/* + Code for configuring and appending tiles. + + Every item and block in the game is + represented in one unified Tile class. + + A prototype of an API is provided + for players who wish to modify the game, + to prevent conflicts from manually setting + sections of an array. +*/ + +var canGravity = [ + +]; + +function Tile(color, id) { + this.color = color; + + this.id = id; + this.number = -1; + this.interactions = []; + this.attributes = {}; + this.attributes.temperature = 0; + this.attributes.conduct = 0.01; + + this.color = (color == 'none') ? [181,204,253,1/255] : color.replace(/^[^\(]+\(/,'').replace(/\)$/,'').split(',').map(x => 1 * x) + if (color == 'random') this.color = [-1,-1,-1]; // ugly and hard-coded, but somehow faster? + + /* + Interactions are used for dynamic functions that + depend on world state, while attributes are used + for block attributes that are static, or modified + by other interactions. + + I highly suggest you define certain modifiers + as a prototype of Tile that returns + itself. This allows for prototype chaining + within tile definitions. + */ +} + +function TileManager(row, row2) { + this.tiles = []; + this.row = row; + this.row2 = row2; + this.sel = 0; + + this.used = {}; + +} + +TileManager.prototype.loadSet = function (namespace, tiles) { + let path = namespace.split('/'); + + let elem = document.createElement('a'); + elem.textContent = namespace; + elem.href = `#${namespace}` + this.row.appendChild(elem); + + let elem2 = document.createElement('section'); + elem2.id = namespace + this.row2.appendChild(elem2); + + + for (let tile of tiles) { + tile.namespace = namespace; + tile.number = this.tiles.length; + this.tiles.push(tile); + + canGravity[tile.number] = !tile.attributes.noGravity; + + if (path.indexOf('secret') != -1) continue; + + elem = document.createElement('button'); + elem.textContent = tile.id; + elem2.appendChild(elem); + + elem.addEventListener('click', () => { + this.sel = tile.number; + }) + } +} + +TileManager.prototype.resolveID = function (namespace, name) { + let resolved = this.tiles + .findIndex(tile => + tile.namespace == namespace && + tile.id == name + ); + return resolved +} + +TileManager.prototype.resolve = function (namespace, name) { + let id = this.resolveID(namespace, name); + return this.tiles[id]; +} + +var mainTiles = new TileManager( + document.querySelector('.menu'), + document.querySelector('.buttons') +); + +/* + You can theoretically add more tile managers if desired, + but you probably shouldn't + if you don't know what you are doing. +*/ \ No newline at end of file diff --git a/json/TestMod.json b/json/TestMod.json new file mode 100644 index 0000000..ba4c018 --- /dev/null +++ b/json/TestMod.json @@ -0,0 +1,24 @@ +[ + { + "namespace": "TestMod/CoolStuff", + "content": [ + { + "color": "rgba(69,69,69)", + "name": "Cool Test Item", + "params": [ + { + "func": "cohesion", + "options": [] + }, + { + "func": "gravity", + "options": [ + 50000, + 7 + ] + } + ] + } + ] + } +] \ No newline at end of file