map editor
This commit is contained in:
parent
44334fc385
commit
113c6d105a
15 changed files with 344 additions and 26 deletions
21
client/css/editor.css
Normal file
21
client/css/editor.css
Normal 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;
|
||||
}
|
|
@ -70,4 +70,9 @@ input {
|
|||
border: solid 2px #fff;
|
||||
padding: .25rem;
|
||||
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 |
|
@ -11,7 +11,7 @@
|
|||
<div id="center">
|
||||
<form id="join" autocomplete="off">
|
||||
<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!"/>
|
||||
</form>
|
||||
<div id="lobby">
|
||||
|
|
|
@ -17,8 +17,8 @@ lobby.style.display = "none";
|
|||
join.onsubmit = function (event) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
event.preventDefault();
|
||||
const room_code = this.elements.room_code.value.trim();
|
||||
const player_name = this.elements.name.value.trim();
|
||||
const room_code = document.getElementById("room_code").value;
|
||||
const player_name = document.getElementById("player_name").value;
|
||||
if (room_code == '') {
|
||||
alert('Please enter a room code');
|
||||
return;
|
||||
|
|
35
client/mapeditor.html
Normal file
35
client/mapeditor.html
Normal 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
166
client/src/editor.ts
Normal 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)
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
|
@ -20,7 +20,7 @@ const getTile = (
|
|||
): number => {
|
||||
let x = Math.round(pos.x + ox)
|
||||
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]
|
||||
}
|
||||
|
||||
|
@ -73,13 +73,11 @@ const incrementPos = (
|
|||
pos.x -= speed
|
||||
break
|
||||
case Rotation.EAST:
|
||||
pos.y += speed
|
||||
pos.x += speed
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0
|
||||
|
||||
const updateMovementForPlayer = (
|
||||
map: Map,
|
||||
player: Player,
|
||||
|
@ -91,7 +89,7 @@ const updateMovementForPlayer = (
|
|||
let currentPosition = player.pos
|
||||
|
||||
let turningFrontTile = getTileFrontWithRot(map, currentPosition, inputRot)
|
||||
if (turningFrontTile == 1 || turningFrontTile == 2) {
|
||||
if (turningFrontTile == Tile.WALL || turningFrontTile == Tile.GHOST_WALL) {
|
||||
inputRot = Rotation.NOTHING
|
||||
}
|
||||
|
||||
|
@ -109,7 +107,7 @@ const updateMovementForPlayer = (
|
|||
incrementPos(movePos, moveRot, MOVE_SPEED)
|
||||
|
||||
let frontTile = getTileFrontWithRot(map, currentPosition, moveRot)
|
||||
if (frontTile != 1 && frontTile != 2) {
|
||||
if (frontTile != Tile.WALL && frontTile != Tile.GHOST_WALL) {
|
||||
player.pos = movePos
|
||||
player.moving = true
|
||||
} else {
|
||||
|
|
|
@ -33,7 +33,7 @@ export const updatePlayers = (data: GameState, input: Input) => {
|
|||
data.players[added] ||= {
|
||||
pos: {x: 1, y: 1},
|
||||
inputRotation: Rotation.EAST,
|
||||
moveRotation: Rotation.EAST,
|
||||
moveRotation: Rotation.NOTHING,
|
||||
moving: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ lobby.style.display = "none"
|
|||
join.onsubmit = async function(event) {
|
||||
event.preventDefault()
|
||||
|
||||
const room_code = this.elements.room_code.value.trim()
|
||||
const player_name = this.elements.name.value.trim()
|
||||
const room_code = (<HTMLInputElement>document.getElementById("room_code")).value
|
||||
const player_name = (<HTMLInputElement>document.getElementById("player_name")).value
|
||||
|
||||
if (room_code == '') {
|
||||
alert('Please enter a room code')
|
||||
|
|
|
@ -126,6 +126,24 @@ export const genItems = (map: Map): Items => {
|
|||
let mapData: Maps = {}
|
||||
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 = (
|
||||
width: number,
|
||||
height: number,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
setInput(frame: number, input: Input) {
|
||||
console.log('input', frame, input)
|
||||
this.editFrame(frame, (index: number): void => {
|
||||
let past = this.history[index - 1];
|
||||
if(index === 0) {
|
||||
|
@ -66,7 +65,6 @@ export class Game {
|
|||
}
|
||||
|
||||
setData(frame: number, data: GameState) {
|
||||
console.log('data', frame, data)
|
||||
this.editFrame(frame, (index: number): void => {
|
||||
this.history[index] = {
|
||||
data,
|
||||
|
|
|
@ -80,7 +80,7 @@ export function multiplayer(
|
|||
function update(input: PlayerInput, frame: number) {
|
||||
if(input === undefined) { // used to update the game locally
|
||||
if(hasState) {
|
||||
applyInput({})
|
||||
applyInput({frame})
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { getMap } from "./map.js";
|
||||
import { Items, Players, Rotation, ItemType, Map, Wall, GameState } from "./types.js";
|
||||
|
||||
const ATLAS_TILE_WIDTH = 32
|
||||
import { Items, Players, Rotation, ItemType, Map, Wall, GameState, Tile, ATLAS_TILE_WIDTH } from "./types.js";
|
||||
|
||||
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")
|
||||
const draw_map = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
atlas: CanvasImageSource,
|
||||
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.height = map.height * ATLAS_TILE_WIDTH
|
||||
|
||||
let map_ctx = map_canvas.getContext("2d")
|
||||
draw_map_canvas(map_ctx, atlas, map)
|
||||
if (editor) {
|
||||
draw_debug_sprites(map_ctx, atlas, map)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.drawImage (
|
||||
|
@ -292,7 +352,8 @@ export const startGraphicsUpdater = () => {
|
|||
*/
|
||||
return (
|
||||
data: GameState,
|
||||
frame: number
|
||||
frame: number,
|
||||
editor: boolean = false
|
||||
) => {
|
||||
|
||||
let map = getMap(data.mapId)
|
||||
|
@ -308,7 +369,7 @@ export const startGraphicsUpdater = () => {
|
|||
let ctx = canvas.getContext("2d")
|
||||
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_players(ctx, atlas, data.players, frame)
|
||||
update_style(map.width, map.height)
|
||||
|
|
|
@ -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 {
|
||||
EMPTY,
|
||||
WALL_HZ,
|
||||
|
@ -63,8 +79,8 @@ export type Player = {
|
|||
pos: Vec2,
|
||||
moveRotation: Rotation,
|
||||
inputRotation: Rotation,
|
||||
name?: string,
|
||||
moving: boolean
|
||||
moving: boolean,
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type PlayerInput = {
|
||||
|
|
Loading…
Reference in a new issue