Work on world rendering

This commit is contained in:
Sky Johnson 2024-10-11 18:17:38 -05:00
parent b9a54859ce
commit a89d530cd0
11 changed files with 219 additions and 129 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -324,6 +324,10 @@ span.badge {
background-color: #444c55; background-color: #444c55;
color: white; color: white;
} }
&.green {
background-color: #a6e3a1;
}
} }
.my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; } .my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; }
@ -517,3 +521,14 @@ body::-webkit-scrollbar-thumb {
border-color: #3D444C #2F353B #2C3137; border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset; box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
} }
#canvas-container {
& > canvas {
display: block;
width: 100%;
height: 440px;
image-rendering: pixelated;
image-rendering: crisp-edges;
background-color: rgba(0, 0, 0, 0.5);
}
}

View File

@ -86,8 +86,6 @@
& > span.selected { & > span.selected {
display: none; display: none;
margin-left: 1rem;
color: #a6e3a1;
} }
} }

View File

@ -15,42 +15,46 @@ function world_controller_get()
*/ */
function world_controller_move_post() function world_controller_move_post()
{ {
auth_only_and_must_have_character(); csrf_ensure(); /*
This endpoint is used to move the character around the world. The client sends a POST request with the direction
they want to move the character. The server will update the character's position in the database and return the
new position to the client.
We should only be using this endpoint as an AJAX request from the world page. Since we don't need all the character's
data to move them, we can just get and update their lcoation using the user's currently selected character ID.
*/
ajax_only(); auth_only(); csrf_ensure();
define('directions', [
[0, -1], // Up
[0, 1], // Down
[-1, 0], // Left
[1, 0] // Right
]);
// direction must exist // direction must exist
$direction = $_POST['direction'] ?? false; $d = (int) $_POST['direction'] ?? -1;
// direction must be valid; 0-3 are sent from the client
if (!is_numeric($direction) || $direction < 0 || $direction > 3 || $direction === false) router_error(999);
// Update the character's position // Update the character's position
// 0 = up, 1 = down, 2 = left, 3 = right // 0 = up, 1 = down, 2 = left, 3 = right
$x = location('x'); $x = location('x');
$y = location('y'); $y = location('y');
switch ($direction) { if (isset(directions[$d])) {
case 0: $y--; break; $x += directions[$d][0];
case 1: $y++; break; $y += directions[$d][1];
case 2: $x--; break; } else {
case 3: $x++; break; router_error(999);
} }
// Update the character's position
$r = db_query(db_live(), 'UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [ $r = db_query(db_live(), 'UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [
':x' => $x, ':x' => $x,
':y' => $y, ':y' => $y,
':c' => char('id') ':c' => user('char_id')
]); ]);
// If the query failed, throw an error
if ($r === false) throw new Exception('Failed to move character. (wcmp)'); if ($r === false) throw new Exception('Failed to move character. (wcmp)');
// If this is an HTMX request, return the new world page json_response(['x' => $x, 'y' => $y]);
if (is_htmx()) {
echo render('pages/world/base');
exit;
}
// Redirect back to the world page
redirect('/world');
} }

View File

@ -59,12 +59,7 @@ function csrf()
*/ */
function csrf_verify($token) function csrf_verify($token)
{ {
if (hash_equals($_SESSION['csrf'] ?? '', $token)) { return hash_equals($_SESSION['csrf'] ?? '', $token);
$_SESSION['csrf'] = token();
return true;
}
return false;
} }
/** /**
@ -196,7 +191,7 @@ function location($field = '')
$GLOBALS['location'] = db_query( $GLOBALS['location'] = db_query(
db_live(), db_live(),
"SELECT * FROM char_locations WHERE char_id = :c", "SELECT * FROM char_locations WHERE char_id = :c",
[':c' => char('id')] [':c' => user('char_id')]
)->fetchArray(SQLITE3_ASSOC); )->fetchArray(SQLITE3_ASSOC);
} }
@ -254,7 +249,33 @@ function ce($condition, $value, $or = '')
/** /**
* Get whether the request is an HTMX request. * Get whether the request is an HTMX request.
*/ */
function is_htmx(): bool function is_htmx()
{ {
return isset($_SERVER['HTTP_HX_REQUEST']); return isset($_SERVER['HTTP_HX_REQUEST']);
} }
/**
* Get whether the request is an AJAX (fetch) request.
*/
function is_ajax()
{
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
/**
* Limit a request to AJAX only.
*/
function ajax_only()
{
if (!is_ajax()) router_error(418);
}
/**
* Return a JSON response with the given data.
*/
function json_response($data)
{
header('Content-Type: application/json');
echo json_encode($data);
exit;
}

View File

@ -1,5 +1,7 @@
<?php <?php
define('DBP', SRC . '/../database');
/** /**
* Open a connection to a database. * Open a connection to a database.
*/ */
@ -22,7 +24,7 @@ function db_open($path)
*/ */
function db_auth() function db_auth()
{ {
return $GLOBALS['db_auth'] ??= db_open(SRC . '/../database/auth.db'); return $GLOBALS['db_auth'] ??= db_open(DBP . '/auth.db');
} }
/** /**
@ -30,7 +32,7 @@ function db_auth()
*/ */
function db_live() function db_live()
{ {
return $GLOBALS['db_live'] ??= db_open(SRC . '/../database/live.db'); return $GLOBALS['db_live'] ??= db_open(DBP . '/live.db');
} }
@ -39,7 +41,7 @@ function db_live()
*/ */
function db_fights() function db_fights()
{ {
return $GLOBALS['db_fights'] ??= db_open(SRC . '/../database/fights.db'); return $GLOBALS['db_fights'] ??= db_open(DBP . '/fights.db');
} }
@ -48,7 +50,7 @@ function db_fights()
*/ */
function db_blueprints() function db_blueprints()
{ {
return $GLOBALS['db_blueprints'] ??= db_open(SRC . '/../database/blueprints.db'); return $GLOBALS['db_blueprints'] ??= db_open(DBP . '/blueprints.db');
} }
/** /**

View File

@ -3,6 +3,6 @@
<label for="char_<?= $id ?>"> <label for="char_<?= $id ?>">
<?= $char['name'] ?> <?= $char['name'] ?>
<span class="badge"><?= $char['level'] ?></span> <span class="badge"><?= $char['level'] ?></span>
<span class="selected">Active</span> <span class="badge green selected">Active</span>
</label> </label>
</div> </div>

View File

@ -1,113 +1,163 @@
<div id="target_world_map">
<h1>World</h1> <h1>World</h1>
<p>Use WASD keys to move the character</p> <p>Use WASD keys to move the character</p>
<p>Current location: <?= location('x') ?>, <?= location('y') ?></p> <p>Current location: <span id="char_x"><?= location('x') ?></span>, <span id="char_y"><?= location('y') ?></span></p>
<canvas id="canvas"></canvas> <div id="canvas-container">
<canvas id="canvas"></canvas>
<form hx-post="/move" hx-target="target_world_map" hx-swap="outerHTML" id="form_move"> </div>
<?= csrf_field() ?>
<input type="hidden" name="direction" value="">
</form>
<script> <script>
const canvas = document.getElementById('canvas'); const game = {
const ctx = canvas.getContext('2d'); canvas: document.getElementById('canvas'),
csrf: '<?= csrf() ?>',
let char_x = <?= location('x') ?>; tiles: {
let char_y = <?= location('y') ?>; size: 32,
colors: {
// Configuration 0: '#fff',
const TILE_SIZE = 40; // Fixed size for each tile 1: '#f00'
// Sample tile map (0 = empty, 1 = filled)
const tileMap = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
// Turn the value at the player's coordinates to be 1 in the map array
tileMap[char_y][char_x] = 1;
const tileColors = {
0: '#fff', // Empty tile
1: '#333' // Wall tile
};
function resizeCanvas() {
// Calculate the actual dimensions needed for the map
const mapWidth = tileMap[0].length * TILE_SIZE;
const mapHeight = tileMap.length * TILE_SIZE;
// Get the container's width
const containerWidth = canvas.parentElement.clientWidth;
// Calculate the scale factor to fit the map width to the container
const scale = Math.min(1, containerWidth / mapWidth);
// Set canvas size using the scale factor
canvas.width = mapWidth * scale;
canvas.height = mapHeight * scale;
// Scale the context to maintain tile dimensions
ctx.scale(scale, scale);
// Render the map after resize
renderMap();
}
function renderMap() {
// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw each tile
for (let row = 0; row < tileMap.length; row++) {
for (let col = 0; col < tileMap[row].length; col++) {
const tileType = tileMap[row][col];
const x = col * TILE_SIZE;
const y = row * TILE_SIZE;
ctx.fillStyle = tileColors[tileType];
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE);
// Draw tile borders
ctx.strokeStyle = '#999';
ctx.strokeRect(x, y, TILE_SIZE, TILE_SIZE);
} }
} }
} }
// Initial setup const map = [
resizeCanvas(); [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]
// Handle window resizing let loc_span = {
window.addEventListener('resize', resizeCanvas); x: document.getElementById('char_x'),
y: document.getElementById('char_y')
}
// On WASD key press, send a POST request to move the character using the form let player = { x: <?= location('x') ?>, y: <?= location('y') ?> }
window.addEventListener('keydown', (e) => { let camera = { x: 0, y: 0 }
if (!['w', 'a', 's', 'd'].includes(e.key)) return; let visible = { x: 0, y: 0 }
el = document.querySelector('input[name="direction"]'); function updateCanvasSize() {
game.canvas.width = game.canvas.clientWidth
game.canvas.height = game.canvas.clientHeight
visible.x = Math.ceil(game.canvas.width / game.tiles.size)
visible.y = Math.ceil(game.canvas.height / game.tiles.size)
updateCamera()
render()
}
function setupEventListeners() {
window.addEventListener('resize', updateCanvasSize)
window.addEventListener('keydown', handleKeyPress)
}
// Handle keyboard input
function handleKeyPress(e) {
let moved = false;
const newPos = { ...player }
// update the direction input to be 0-3 based on the key pressed
// 0 = up, 1 = down, 2 = left, 3 = right // 0 = up, 1 = down, 2 = left, 3 = right
el.value = { direction = {
'w': 0, 'w': 0,
's': 1, 's': 1,
'a': 2, 'a': 2,
'd': 3 'd': 3,
'ArrowUp': 0,
'ArrowDown': 1,
'ArrowLeft': 2,
'ArrowRight': 3
}[e.key]; }[e.key];
htmx.trigger(document.getElementById('form_move'), 'submit'); if (direction !== undefined) {
}); // Execute a POST request to /move. If successful, the server will return a new x,y position
fetch('/move', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: `direction=${direction}&csrf=${game.csrf}`
}).then(response => {
if (response.ok) {
response.json().then(data => {
map[player.y][player.x] = 0
map[data.y][data.x] = 1
player = data
loc_span.x.textContent = player.x
loc_span.y.textContent = player.y
updateCamera()
render()
});
} else {
throw new Error('Failed to move character');
}
})
}
}
// Update camera position
function updateCamera() {
camera.x = player.x * game.tiles.size - canvas.width / 2;
camera.y = player.y * game.tiles.size - canvas.height / 2;
// Clamp camera to map bounds
camera.x = Math.max(0, Math.min(camera.x,
map[0].length * game.tiles.size - canvas.width));
camera.y = Math.max(0, Math.min(camera.y,
map.length * game.tiles.size - canvas.height));
}
// Render the game
function render() {
const ctx = game.canvas.getContext('2d')
ctx.clearRect(0, 0, game.canvas.width, game.canvas.height)
// Calculate visible tile range
const startTileX = Math.floor(camera.x / game.tiles.size)
const startTileY = Math.floor(camera.y / game.tiles.size)
const endTileX = startTileX + visible.x + 1
const endTileY = startTileY + visible.y + 1
// Only render visible tiles
for (let y = startTileY; y < endTileY; y++) {
if (y >= map.length) continue
for (let x = startTileX; x < endTileX; x++) {
if (x >= map[0].length) continue
const screenX = x * game.tiles.size - camera.x
const screenY = y * game.tiles.size - camera.y
if (map[y][x] === 1) {
// Draw player
ctx.fillStyle = 'red';
ctx.fillRect(screenX, screenY, game.tiles.size, game.tiles.size)
} else if (map[y][x] === 2) {
// Draw wall
ctx.fillStyle = 'black';
ctx.fillRect(screenX, screenY, game.tiles.size, game.tiles.size)
} else {
// Draw empty tile
ctx.strokeStyle = 'gray';
ctx.fillStyle = 'white';
ctx.fillRect(screenX, screenY, game.tiles.size, game.tiles.size)
ctx.strokeRect(screenX, screenY, game.tiles.size, game.tiles.size)
}
}
}
}
window.addEventListener('load', () => {
updateCanvasSize()
setupEventListeners()
render()
})
</script> </script>
</div>