This commit is contained in:
zeno 2024-02-28 11:59:54 -05:00
commit 6b926e53bf
19 changed files with 1365 additions and 0 deletions

1
CNAME Normal file
View file

@ -0,0 +1 @@
altboxels.qazox.dev

4
README.md Normal file
View file

@ -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.

44
css/core.css Normal file
View file

@ -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;
}

80
index.html Normal file
View file

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
<!-- Metadata -->
<title>Altboxels</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta
name="description"
content="Altboxels is a pixel sandbox game that enables anyone to empower their imagination. It shares many characteristics with Sandboxels, the Powder Toy, among other games in the genre. Many features are custom-built, including the physics engine!"
>
<meta name="keywords" content="sandbox, falling sand, powder toy">
<meta name="author" content="qazox">
<link rel="stylesheet" href="css/core.css">
</head>
<body>
<h1>Altboxels</h1>
<canvas id='main' style=""></canvas>
<canvas id='alt' style='display: none;'></canvas>
<span class='info'>
N/A
</span>
<div class='menu2'>
<section>
<a href='https://discord.gg/wtBVte4Syu'>Chat</a>
<a href='https://github.com/qazox/altboxels'>Source</a>
<a href='https://altboxels.qazox.dev/'>Website</a>
<a href='https://abc.qazox.dev/'>Community</a>
</section>
<section>
<button onclick="handler.noTick = !handler.noTick">Pause</button>
<button onclick="openMods()">Mods</button>
<button onclick="save()">Save</button>
<button onclick="load()">Load</button>
<button onclick="canvas.radius++;">+Rad</button>
<button onclick="canvas.radius = Math.max(canvas.radius - 1,0);">-Rad</button>
</section>
</div>
<div class='menu'>
</div>
<div class='buttons'>
</div>
<textarea id='code'>
(save data here)
</textarea>
<!-- Core code -->
<script src="js/tile.js"></script>
<script src="js/event.js"></script>
<!-- Tile modifiers -->
<script src="js/gravity.js"></script>
<script src="js/cohesion.js"></script>
<script src="js/combine.js"></script>
<script src="js/conway.js"></script>
<script src="js/temperature.js"></script>
<script src="js/state.js"></script>
<script src="js/clone.js"></script>
<!-- Tile sets -->
<script src="js/core_blocks.js"></script>
<!-- Game loop -->
<script src="js/tick_handler.js"></script>
<script src="js/core.js"></script>
<!-- Mod loader -->
<script src="js/loader.js"></script>
<!-- Save/load -->
<script src="js/save_load.js"></script>
</body>
</html>

60
js/clone.js Normal file
View file

@ -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;
}

73
js/cohesion.js Normal file
View file

@ -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;
}

53
js/combine.js Normal file
View file

@ -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;
}

49
js/conway.js Normal file
View file

@ -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;
}

218
js/core.js Normal file
View file

@ -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);

239
js/core_blocks.js Normal file
View file

@ -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');

14
js/event.js Normal file
View file

@ -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;
}
}

99
js/gravity.js Normal file
View file

@ -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;
}

55
js/loader.js Normal file
View file

@ -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())
}

113
js/save_load.js Normal file
View file

@ -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;
})()
}

27
js/state.js Normal file
View file

@ -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;
}

49
js/temperature.js Normal file
View file

@ -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;
}

55
js/tick_handler.js Normal file
View file

@ -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;
}

108
js/tile.js Normal file
View file

@ -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.
*/

24
json/TestMod.json Normal file
View file

@ -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
]
}
]
}
]
}
]