Compare commits

...

15 Commits

Author SHA1 Message Date
HyperOnion f847d57827 create readme. 2023-06-24 14:12:33 +02:00
HyperOnion e214888ed5 add more temp controls for experimenting. 2023-06-24 14:12:33 +02:00
HyperOnion da9db456d8 tiny refactor of entity code. 2023-06-24 14:12:33 +02:00
HyperOnion 2335ffd48e handle out of bounds positions in tile manipulation functions. 2023-06-24 14:12:33 +02:00
HyperOnion d80c8b399c make box checkerboard floors line up nicely. 2023-06-24 14:12:33 +02:00
HyperOnion c4d6e854a5 create basic paint tile type and some controls for creating paint. 2023-06-24 14:12:33 +02:00
HyperOnion e912451423 recursively draw box contents inside of them. 2023-06-24 14:12:33 +02:00
HyperOnion 1be1a39ca0 implement basic box entering and exiting in entity movement. 2023-06-24 14:12:33 +02:00
HyperOnion b80842ac46 refactor world code to no longer assume the player entity:s pov. 2023-06-24 14:12:33 +02:00
HyperOnion 34970bc205 begin refactoring movement logic out of player code into entity code. 2023-06-24 14:12:33 +02:00
HyperOnion 5de18d1a57 remove dead comment. 2023-06-24 14:12:33 +02:00
HyperOnion c3eceaefcb change canvas size to be a power of 9. 2023-06-24 14:12:33 +02:00
HyperOnion b4deea63ce create entity module and refactor player into entity. 2023-06-24 14:12:33 +02:00
HyperOnion d81d906ed8 implement basic box functionality. 2023-06-24 14:12:33 +02:00
HyperOnion 17e25281cd basic player movement and general cleanup. 2023-06-24 14:12:33 +02:00
10 changed files with 300 additions and 48 deletions

18
README.md Normal file
View File

@ -0,0 +1,18 @@
# overview
the player character is currently the dark green square. creating new tiles creates them where you're standing, so you have to move out of the way to see them.
moving onto boxes makes you enter them. right now, this doesn't change your position; in the future entering a box from e.g. the left side will make you move to its left edge.
moving out of bounds makes you exit the box, if you're in one. this also doesn't change your position yet. if you exit at a position where there's another box in the one you exit into, you'll immediately enter that box.
# controls
- arrow keys: move around.
- QWEASD: paint with RGBYCM colors, respectively.
- space: create box.
- backspace: delete/clear/empty tiles to your left and right, and where you're standing.
# how to play
run a server in the root directory and open `index.html` in a web browser.

View File

@ -10,5 +10,5 @@
</head>
<body>
<canvas id="canvas" width="576" height="576"></canvas>
<canvas id="canvas" width="729" height="729"></canvas>
</body>

63
js/entity.mjs Normal file
View File

@ -0,0 +1,63 @@
import * as world from "./world.mjs";
const BOX_SIZE = world.BOX_SIZE;
const entities = [];
export function create (box, x = 0, y = 0, color = "#fff") {
const entity = {
id: entities.length,
box,
x,
y,
color,
};
entities.push(entity);
return entity;
}
export function destroy (id) {
// returns the destroyed entity, idk if that makes sense.
return entities.splice(id, 1)[0];
}
export function for_each (callback) {
entities.forEach(entity => {
callback(entity, entity.id);
});
}
function clamp_pos (entity) {
if (entity.x < 0) entity.x = 0;
if (entity.x >= BOX_SIZE) entity.x = BOX_SIZE - 1;
if (entity.y < 0) entity.y = 0;
if (entity.y >= BOX_SIZE) entity.y = BOX_SIZE - 1;
return entity;
}
export function set_pos (entity, x, y) {
entity.x = x;
entity.y = y;
if (out_of_bounds(entity)) {
if (entity.box.parent) {
entity.box = entity.box.parent;
// TODO: exit from side of box.
}
clamp_pos(entity);
}
if (world.get_tile(entity.box, entity.x, entity.y)?.type === "box") {
entity.box = world.get_tile(entity.box, entity.x, entity.y).box;
// TODO: enter at edge of box.
}
}
export function move (entity, d_x, d_y) {
set_pos(entity, entity.x + d_x, entity.y + d_y);
}
function out_of_bounds (entity) {
return world.out_of_bounds(entity.x, entity.y);
}

View File

@ -1,25 +1,70 @@
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
import * as graphics from "./graphics_core.mjs";
export * from "./graphics_core.mjs";
export function set_size (w, h) {
canvas.width = w;
canvas.height = h;
import { range, iter_2d } from "./utils/range.mjs";
import * as entity from "./entity.mjs";
import * as world from "./world.mjs";
const canvas = document.getElementById("canvas");
const canvas_context = canvas.getContext("2d");
graphics.set_size(canvas.width, canvas.height);
const BASE_SCALE = 81;
export function draw_tile (x, y, size = BASE_SCALE) {
graphics.fill_rect(x, y, size, size);
}
export function set_color (color) {
ctx.fillStyle = color;
ctx.strokeStyle = color;
function checkerboard (x, y) {
return ((x ^ y) & 1) === 0;
}
export function clear (color = "#000") {
set_color(color);
ctx.fillRect(0, 0, canvas.width, canvas.height);
export function draw_floor (start_x, start_y, scale, invert = false) {
iter_2d(range(0, world.BOX_SIZE - 1), (x, y) => {
if (checkerboard(x, y) !== invert) {
graphics.set_color("#aaa");
} else {
graphics.set_color("#888");
}
const [visual_x, visual_y] = project_pos(x, y, scale);
draw_tile(start_x + visual_x, start_y + visual_y, scale);
});
}
export function fill_rect (x, y, w, h) {
ctx.fillRect(x, y, w, h);
export function draw_world (box, start_x = 0, start_y = 0, scale = BASE_SCALE) {
draw_floor(start_x, start_y, scale, !checkerboard(start_x, start_y));
iter_2d(range(0, world.BOX_SIZE - 1), (x, y) => {
const tile = world.get_tile(box, x, y);
const [visual_x, visual_y] = project_pos(x, y, scale);
switch (tile.type) {
case "box": {
draw_world(tile.box, start_x + visual_x, start_y + visual_y, scale / world.BOX_SIZE);
break;
}
case "paint": {
graphics.set_color(tile.color);
draw_tile(start_x + visual_x, start_y + visual_y, scale);
break;
}
}
});
entity.for_each((entity, id) => {
if (entity.box !== box) return;
graphics.set_color(entity.color);
const [visual_x, visual_y] = project_pos(entity.x, entity.y, scale);
draw_tile(start_x + visual_x, start_y + visual_y, scale);
});
}
export function render (target_canvas) {
target_canvas.drawImage(canvas, 0, 0);
function project_pos (x, y, scale) {
return [x, y].map(a => a * scale);
}
export function render () {
graphics.render(canvas_context);
}

25
js/graphics_core.mjs Normal file
View File

@ -0,0 +1,25 @@
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
export function set_size (w, h) {
canvas.width = w;
canvas.height = h;
}
export function set_color (color) {
ctx.fillStyle = color;
ctx.strokeStyle = color;
}
export function clear (color = "#000") {
set_color(color);
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
export function fill_rect (x, y, w, h) {
ctx.fillRect(x, y, w, h);
}
export function render (target_canvas) {
target_canvas.drawImage(canvas, 0, 0);
}

11
js/input.mjs Normal file
View File

@ -0,0 +1,11 @@
export function on_press (key, callback) {
document.body.addEventListener("keydown", e => {
if (e.key === key) callback(key);
});
}
export function on_release (key, callback) {
document.body.addEventListener("keyup", e => {
if (e.key === key) callback(key);
});
}

View File

@ -1,27 +1,15 @@
import * as graphics from "./graphics.mjs";
import { range } from "./utils/range.mjs";
import { BOX_SIZE, get_tile } from "./world.mjs";
import { get_player_box } from "./player.mjs";
const canvas = document.getElementById("canvas");
const canvas_context = canvas.getContext("2d");
graphics.set_size(canvas.width, canvas.height);
const BOX_SIZE = 9;
const SCALE = 64;
function draw_tile (x, y) {
graphics.fill_rect(x * SCALE, y * SCALE, SCALE, SCALE);
function render (timestamp) {
graphics.clear();
graphics.draw_world(get_player_box());
graphics.render();
requestAnimationFrame(render);
}
function draw_box_floor () {
graphics.clear("#888");
graphics.set_color("#aaa");
for (const x in range(0, BOX_SIZE)) {
for (const y in range(0, BOX_SIZE)) {
if (((x ^ y) & 1) === 0)
draw_tile(x, y);
}
}
}
draw_box_floor();
graphics.render(canvas_context);
requestAnimationFrame(render);

65
js/player.mjs Normal file
View File

@ -0,0 +1,65 @@
import * as world from "./world.mjs";
import { on_press } from "./input.mjs";
import * as entity from "./entity.mjs";
const player = entity.create(world.get_root(), 2, 1, "#080");
export function move (d_x, d_y) {
entity.move(player, d_x, d_y);
}
on_press("ArrowLeft", _ => {
move(-1, 0);
});
on_press("ArrowRight", _ => {
move(1, 0);
});
on_press("ArrowUp", _ => {
move(0, -1);
});
on_press("ArrowDown", _ => {
move(0, 1);
});
on_press(" ", _ => {
world.set_tile(player.box, player.x, player.y, {
type: "box",
box: world.create_box(player.box),
});
});
function clear_tile (x, y) {
world.set_tile(player.box, x, y, {
type: "empty",
});
}
on_press("Backspace", _ => {
clear_tile(player.x, player.y);
clear_tile(player.x + 1, player.y);
clear_tile(player.x - 1, player.y);
});
export function get_player_box () {
return player.box;
}
function set_painting_key (key, color) {
on_press(key, _ => {
world.set_tile(player.box, player.x, player.y, {
type: "paint",
color,
});
});
}
set_painting_key("q", "#f00");
set_painting_key("w", "#0f0");
set_painting_key("e", "#00f");
set_painting_key("a", "#ff0");
set_painting_key("s", "#0ff");
set_painting_key("d", "#f0f");

View File

@ -1,16 +1,8 @@
export {
is_in_range,
clamp,
range,
};
function is_in_range (number, min, max) {
export function is_in_range (number, min, max) {
return number >= min && number <= max;
}
function clamp (number, min, max) {
export function clamp (number, min, max) {
return number < min
? min
: number > max
@ -18,10 +10,18 @@ function clamp (number, min, max) {
: number;
}
function range (start, end) {
export function range (start, end) {
const result = [];
for (let n = start; n <= end; n++) {
result.push(n);
}
return result;
}
export function iter_2d (range_1d, callback) {
for (const y in range_1d) {
for (const x in range_1d) {
callback(x, y);
}
}
}

37
js/world.mjs Normal file
View File

@ -0,0 +1,37 @@
export const BOX_SIZE = 9;
export const CENTER = Math.floor(BOX_SIZE / 2);
function create_world (size) {
return {
tiles: Array(BOX_SIZE).fill(0).map(_ => Array(BOX_SIZE).fill(0)),
};
}
const world = create_world(BOX_SIZE);
export function set_tile (world, x, y, tile) {
if (out_of_bounds(x, y)) return;
world.tiles[x][y] = tile;
}
export function get_tile (world, x, y) {
if (out_of_bounds(x, y)) return { type: "out_of_bounds" };
return world.tiles[x][y];
}
export function create_box (parent) {
return {
...create_world(BOX_SIZE),
parent,
};
}
export function get_root () {
return world;
}
export function out_of_bounds (x, y) {
return x < 0 || x >= BOX_SIZE ||
y < 0 || y >= BOX_SIZE
}