map editor

This commit is contained in:
Freya Murphy 2023-06-17 01:18:16 -04:00
parent 44334fc385
commit 113c6d105a
15 changed files with 344 additions and 26 deletions

21
client/css/editor.css Normal file
View file

@ -0,0 +1,21 @@
canvas {
box-shadow: inset 0 0 1px red;
}
#sidebar {
background-color: #191919;
position: absolute;
display: flex;
flex-direction: column;
border: 1px solid;
padding: 1rem;
}
#mapgen {
display: flex;
flex-direction: column;
}
#export {
margin-top: 1rem;
}

View file

@ -70,4 +70,9 @@ input {
border: solid 2px #fff; border: solid 2px #fff;
padding: .25rem; padding: .25rem;
margin-bottom: .215rem; margin-bottom: .215rem;
box-sizing:content-box;
}
p {
padding: .25rem;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -11,7 +11,7 @@
<div id="center"> <div id="center">
<form id="join" autocomplete="off"> <form id="join" autocomplete="off">
<input type="text" id="room_code" name="room_code" placeholder="Room Code"> <input type="text" id="room_code" name="room_code" placeholder="Room Code">
<input type="text" id="name" name="name" placeholder="Player Name"> <input type="text" id="player_name" name="name" placeholder="Player Name">
<input type="submit" value="Join!"/> <input type="submit" value="Join!"/>
</form> </form>
<div id="lobby"> <div id="lobby">

View file

@ -17,8 +17,8 @@ lobby.style.display = "none";
join.onsubmit = function (event) { join.onsubmit = function (event) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
event.preventDefault(); event.preventDefault();
const room_code = this.elements.room_code.value.trim(); const room_code = document.getElementById("room_code").value;
const player_name = this.elements.name.value.trim(); const player_name = document.getElementById("player_name").value;
if (room_code == '') { if (room_code == '') {
alert('Please enter a room code'); alert('Please enter a room code');
return; return;

35
client/mapeditor.html Normal file
View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/main.css"/>
<link rel="stylesheet" href="css/editor.css"/>
<script>var exports = {};</script>
</head>
<body>
<img src="img/atlas.png" id="atlas" style="display: none;"/>
<canvas id="canvas" style="display: none;"></canvas>
<style id="style"></style>
<div id="center">
<form id="mapgen" autocomplete="off">
<input type="text" id="width" name="width" placeholder="Map Width">
<input type="text" id="height" name="height" placeholder="Map Height">
<input type="submit" value="Create"/>
</form>
</div>
<div id="sidebar">
<p>W: Place Wall</p>
<p>G: Place Ghost Wall</p>
<p>F: Place Food</p>
<p>1: Place Pac Spawn 1</p>
<p>2: Place Pac Spawn 2</p>
<p>3: Place Pac Spawn 3</p>
<p>4: Place Pac Spawn 4</p>
<p>T: Place THICC Dot</p>
<p>I: Place Initial Dot</p>
<p>C: Clear Tile</p>
<p>Q: Toggle Sidebar</p>
<input type="button" id="export" value="Export Map">
</div>
<script src="js/editor.js" type="module"></script>
</body>
</html>

166
client/src/editor.ts Normal file
View file

@ -0,0 +1,166 @@
import { genMap } from "./map.js"
import { startGraphicsUpdater } from "./renderer.js"
import { GameState, Vec2, ATLAS_TILE_WIDTH, Tile } from "./types.js"
const mapgen = document.getElementById("mapgen")
const sidebar = document.getElementById("sidebar")
sidebar.style.display = "none"
mapgen.onsubmit = async function(event) {
event.preventDefault()
const width_str = (<HTMLInputElement>document.getElementById("width")).value
const height_str = (<HTMLInputElement>document.getElementById("height")).value
const width = parseInt(width_str)
const height = parseInt(height_str)
if (!width || width < 3 || !height || height < 3) {
alert('Invalid numbers or dimensions too small')
return
}
mapgen.style.display = "none"
runMapEditor(width, height)
}
const startKeyListener = () => {
let keys = {}
window.addEventListener("keydown", ev => {
if(ev.repeat) {
return;
}
if (ev.code == "KeyQ") {
if (sidebar.style.display === "none") {
sidebar.style.display = ""
} else {
sidebar.style.display = "none"
}
}
keys[ev.code] = true
});
window.addEventListener("keyup", ev => {
if (ev.repeat) {
return
}
keys[ev.code] = false
})
return () => {
return keys
}
}
const trackMouseMovement = () => {
let pos: Vec2 = {x : 0, y: 0}
window.addEventListener("mousemove", ev => {
pos = {x: ev.x, y: ev.y}
})
return () => {
return pos
}
}
const getTilePos = (width: number, height: number, mousePos: Vec2): Vec2 => {
const canvas = document.getElementById("canvas") as HTMLCanvasElement
const canvasRect = canvas.getBoundingClientRect()
let posX = mousePos.x - canvasRect.x
let posY = mousePos.y - canvasRect.y
let percentX = posX / canvasRect.width
let percentY = posY / canvasRect.height
return {
x: Math.floor(percentX * width),
y: Math.floor(percentY * height)
}
}
const checkInputs = (pressed: {[key: string]: boolean}): Tile => {
if (pressed["KeyW"]) {
return Tile.WALL
} else if (pressed["KeyG"]) {
return Tile.GHOST_WALL
} else if (pressed["KeyF"]) {
return Tile.FOOD
} else if (pressed["Digit1"]) {
return Tile.PLAYER_SPAWN_1
} else if (pressed["Digit2"]) {
return Tile.PLAYER_SPAWN_2
} else if (pressed["Digit3"]) {
return Tile.PLAYER_SPAWN_3
} else if (pressed["Digit4"]) {
return Tile.PLAYER_SPAWN_4
} else if (pressed["KeyT"]) {
return Tile.THICC_DOT
} else if (pressed["KeyI"]) {
return Tile.INITIAL_DOT
} else if (pressed["KeyC"]) {
return Tile.EMPTY
}
return undefined
}
const checkBounds = (tilePos: Vec2, width: number, height: number) => {
if (tilePos.x < 0 || tilePos.x >= width || tilePos.y < 0 || tilePos.y >= height) return false
return true
}
const runMapEditor = (width: number, height: number) => {
sidebar.style.display = ""
let data: number[] = new Array(width * height).fill(0)
genMap(width, height, data, Tile.EMPTY)
let state: GameState = {
started: true,
input: {},
players: {},
items: {},
mapId: 0
}
let frame = 0
const updateGraphics = startGraphicsUpdater()
const getInput = startKeyListener()
const getMousePos = trackMouseMovement()
const loop = () => {
const mousePos = getMousePos()
const tilePos = getTilePos(width, height, mousePos)
const pressed = getInput()
const tile = checkInputs(pressed)
if (tile !== undefined && checkBounds(tilePos, width, height)) {
let current = data[tilePos.y * width + tilePos.x];
if (current != tile) {
data[tilePos.y * width + tilePos.x] = tile
genMap(width, height, data, 0)
}
}
updateGraphics(state, frame, true)
requestAnimationFrame(loop)
}
requestAnimationFrame(loop)
}

View file

@ -1,5 +1,5 @@
import { getMap } from "../map.js" import { getMap } from "../map.js"
import { Vec2, Map, Rotation, Key, Player, GameState } from "../types.js" import { Vec2, Map, Rotation, Key, Player, GameState, Tile } from "../types.js"
const MOVE_SPEED = .1 const MOVE_SPEED = .1
@ -20,7 +20,7 @@ const getTile = (
): number => { ): number => {
let x = Math.round(pos.x + ox) let x = Math.round(pos.x + ox)
let y = Math.round(pos.y + oy) let y = Math.round(pos.y + oy)
if (x < 0 || x >= map.width || y < 0 || y >= map.height) return 1 if (x < 0 || x >= map.width || y < 0 || y >= map.height) return Tile.WALL
return map.data[y * map.width + x] return map.data[y * map.width + x]
} }
@ -73,13 +73,11 @@ const incrementPos = (
pos.x -= speed pos.x -= speed
break break
case Rotation.EAST: case Rotation.EAST:
pos.y += speed pos.x += speed
break break
} }
} }
let i = 0
const updateMovementForPlayer = ( const updateMovementForPlayer = (
map: Map, map: Map,
player: Player, player: Player,
@ -91,7 +89,7 @@ const updateMovementForPlayer = (
let currentPosition = player.pos let currentPosition = player.pos
let turningFrontTile = getTileFrontWithRot(map, currentPosition, inputRot) let turningFrontTile = getTileFrontWithRot(map, currentPosition, inputRot)
if (turningFrontTile == 1 || turningFrontTile == 2) { if (turningFrontTile == Tile.WALL || turningFrontTile == Tile.GHOST_WALL) {
inputRot = Rotation.NOTHING inputRot = Rotation.NOTHING
} }
@ -109,7 +107,7 @@ const updateMovementForPlayer = (
incrementPos(movePos, moveRot, MOVE_SPEED) incrementPos(movePos, moveRot, MOVE_SPEED)
let frontTile = getTileFrontWithRot(map, currentPosition, moveRot) let frontTile = getTileFrontWithRot(map, currentPosition, moveRot)
if (frontTile != 1 && frontTile != 2) { if (frontTile != Tile.WALL && frontTile != Tile.GHOST_WALL) {
player.pos = movePos player.pos = movePos
player.moving = true player.moving = true
} else { } else {

View file

@ -33,7 +33,7 @@ export const updatePlayers = (data: GameState, input: Input) => {
data.players[added] ||= { data.players[added] ||= {
pos: {x: 1, y: 1}, pos: {x: 1, y: 1},
inputRotation: Rotation.EAST, inputRotation: Rotation.EAST,
moveRotation: Rotation.EAST, moveRotation: Rotation.NOTHING,
moving: false, moving: false,
}; };

View file

@ -9,9 +9,9 @@ lobby.style.display = "none"
join.onsubmit = async function(event) { join.onsubmit = async function(event) {
event.preventDefault() event.preventDefault()
const room_code = this.elements.room_code.value.trim() const room_code = (<HTMLInputElement>document.getElementById("room_code")).value
const player_name = this.elements.name.value.trim() const player_name = (<HTMLInputElement>document.getElementById("player_name")).value
if (room_code == '') { if (room_code == '') {
alert('Please enter a room code') alert('Please enter a room code')

View file

@ -126,6 +126,24 @@ export const genItems = (map: Map): Items => {
let mapData: Maps = {} let mapData: Maps = {}
let id: number = 0 let id: number = 0
export const genMap = (
width: number,
height: number,
data: number[],
mapId: number,
): Map => {
mapData[mapId] = {
data: structuredClone(data),
walls: genWalls(width, height, data),
width,
height,
id: mapId
}
return mapData[mapId]
}
export const loadMap = ( export const loadMap = (
width: number, width: number,
height: number, height: number,

View file

@ -52,7 +52,6 @@ export class Game {
* If the frame is ahead of the current latest frame, the game will be run until that frame. * If the frame is ahead of the current latest frame, the game will be run until that frame.
*/ */
setInput(frame: number, input: Input) { setInput(frame: number, input: Input) {
console.log('input', frame, input)
this.editFrame(frame, (index: number): void => { this.editFrame(frame, (index: number): void => {
let past = this.history[index - 1]; let past = this.history[index - 1];
if(index === 0) { if(index === 0) {
@ -66,7 +65,6 @@ export class Game {
} }
setData(frame: number, data: GameState) { setData(frame: number, data: GameState) {
console.log('data', frame, data)
this.editFrame(frame, (index: number): void => { this.editFrame(frame, (index: number): void => {
this.history[index] = { this.history[index] = {
data, data,

View file

@ -80,7 +80,7 @@ export function multiplayer(
function update(input: PlayerInput, frame: number) { function update(input: PlayerInput, frame: number) {
if(input === undefined) { // used to update the game locally if(input === undefined) { // used to update the game locally
if(hasState) { if(hasState) {
applyInput({}) applyInput({frame})
} }
return; return;
} }

View file

@ -1,7 +1,5 @@
import { getMap } from "./map.js"; import { getMap } from "./map.js";
import { Items, Players, Rotation, ItemType, Map, Wall, GameState } from "./types.js"; import { Items, Players, Rotation, ItemType, Map, Wall, GameState, Tile, ATLAS_TILE_WIDTH } from "./types.js";
const ATLAS_TILE_WIDTH = 32
const update_style = (width: number, height: number) => { const update_style = (width: number, height: number) => {
@ -257,20 +255,82 @@ const draw_map_canvas = (
} }
const draw_debug_sprites = (
ctx: CanvasRenderingContext2D,
atlas: CanvasImageSource,
map: Map
) => {
for (let y = 0; y < map.height; y++) {
for (let x = 0; x < map.width; x++) {
let tile_type = map.data[y * map.width + x]
let atlas_index: [number, number];
switch (tile_type) {
case Tile.EMPTY:
case Tile.WALL:
continue
case Tile.GHOST_WALL:
atlas_index = [4, 0]
break
case Tile.FOOD:
atlas_index = [3, 0]
break
case Tile.PLAYER_SPAWN_1:
atlas_index = [3, 1]
break
case Tile.PLAYER_SPAWN_2:
atlas_index = [4, 1]
break
case Tile.PLAYER_SPAWN_3:
atlas_index = [3, 2]
break
case Tile.PLAYER_SPAWN_4:
atlas_index = [4, 2]
break
case Tile.THICC_DOT:
atlas_index = [4, 3]
break
case Tile.INITIAL_DOT:
atlas_index = [3, 3]
break
}
draw_sprite (
ctx,
x,
y,
1,
atlas,
atlas_index,
ATLAS_TILE_WIDTH,
0
)
}
}
}
let map_canvas = document.createElement("canvas") let map_canvas = document.createElement("canvas")
const draw_map = ( const draw_map = (
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
atlas: CanvasImageSource, atlas: CanvasImageSource,
map: Map, map: Map,
last: number | undefined last: number | undefined,
editor: boolean
) => { ) => {
if (map.id !== last) { if (map.id !== last || editor) {
map_canvas.width = map.width * ATLAS_TILE_WIDTH map_canvas.width = map.width * ATLAS_TILE_WIDTH
map_canvas.height = map.height * ATLAS_TILE_WIDTH map_canvas.height = map.height * ATLAS_TILE_WIDTH
let map_ctx = map_canvas.getContext("2d") let map_ctx = map_canvas.getContext("2d")
draw_map_canvas(map_ctx, atlas, map) draw_map_canvas(map_ctx, atlas, map)
if (editor) {
draw_debug_sprites(map_ctx, atlas, map)
}
} }
ctx.drawImage ( ctx.drawImage (
@ -292,7 +352,8 @@ export const startGraphicsUpdater = () => {
*/ */
return ( return (
data: GameState, data: GameState,
frame: number frame: number,
editor: boolean = false
) => { ) => {
let map = getMap(data.mapId) let map = getMap(data.mapId)
@ -308,7 +369,7 @@ export const startGraphicsUpdater = () => {
let ctx = canvas.getContext("2d") let ctx = canvas.getContext("2d")
ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.clearRect(0, 0, canvas.width, canvas.height)
draw_map(ctx, atlas, map, last_map_drawn) draw_map(ctx, atlas, map, last_map_drawn, editor)
draw_items(ctx, atlas, data.items) draw_items(ctx, atlas, data.items)
draw_players(ctx, atlas, data.players, frame) draw_players(ctx, atlas, data.players, frame)
update_style(map.width, map.height) update_style(map.width, map.height)

View file

@ -1,4 +1,20 @@
export const ATLAS_TILE_WIDTH = 32
export enum Tile {
EMPTY,
WALL,
GHOST_WALL,
FOOD,
PLAYER_SPAWN_1,
PLAYER_SPAWN_2,
PLAYER_SPAWN_3,
PLAYER_SPAWN_4,
GHOST_SPAWN,
THICC_DOT,
INITIAL_DOT
}
export enum Wall { export enum Wall {
EMPTY, EMPTY,
WALL_HZ, WALL_HZ,
@ -63,8 +79,8 @@ export type Player = {
pos: Vec2, pos: Vec2,
moveRotation: Rotation, moveRotation: Rotation,
inputRotation: Rotation, inputRotation: Rotation,
name?: string, moving: boolean,
moving: boolean name?: string
} }
export type PlayerInput = { export type PlayerInput = {