Compare commits

..

2 Commits

Author SHA1 Message Date
a89d530cd0 Work on world rendering 2024-10-11 18:17:38 -05:00
b9a54859ce src restructure, world map 2024-10-10 19:14:14 -05:00
28 changed files with 318 additions and 69 deletions

Binary file not shown.

View File

@ -324,6 +324,10 @@ span.badge {
background-color: #444c55;
color: white;
}
&.green {
background-color: #a6e3a1;
}
}
.my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; }
@ -414,7 +418,7 @@ span.badge {
left: 0;
}
#debug-query-log {
.debug-query-log {
padding: 2rem;
font-size: 14px;
color: #666;
@ -517,3 +521,14 @@ body::-webkit-scrollbar-thumb {
border-color: #3D444C #2F353B #2C3137;
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 {
display: none;
margin-left: 1rem;
color: #a6e3a1;
}
}

File diff suppressed because one or more lines are too long

View File

@ -38,11 +38,8 @@ router_post($r, '/character/delete', 'char_controller_delete_post');
/*
World
*/
router_get($r, '/world', function () {
auth_only_and_must_have_character();
$GLOBALS['active_nav_tab'] = 'world';
echo page('world/base');
});
router_get($r, '/world', 'world_controller_get');
router_post($r, '/move', 'world_controller_move_post');
/*
Router

View File

@ -6,22 +6,23 @@ session_start();
// Source libraries
require_once SRC . '/helpers.php';
require_once SRC . '/env.php';
require_once SRC . '/database.php';
require_once SRC . '/auth.php';
require_once SRC . '/router.php';
require_once SRC . '/components.php';
require_once SRC . '/render.php';
require_once SRC . '/util/env.php';
require_once SRC . '/util/database.php';
require_once SRC . '/util/auth.php';
require_once SRC . '/util/router.php';
require_once SRC . '/util/components.php';
require_once SRC . '/util/render.php';
// Database models
require_once SRC . '/models/user.php';
require_once SRC . '/models/session.php';
require_once SRC . '/models/token.php';
require_once SRC . '/models/char.php';
require_once SRC . '/model/user.php';
require_once SRC . '/model/session.php';
require_once SRC . '/model/token.php';
require_once SRC . '/model/char.php';
// Controllers
require_once SRC . '/controllers/char.php';
require_once SRC . '/controllers/auth.php';
require_once SRC . '/controller/char.php';
require_once SRC . '/controller/auth.php';
require_once SRC . '/controller/world.php';
// Track the start time of the request
define('START_TIME', microtime(true));

60
src/controller/world.php Normal file
View File

@ -0,0 +1,60 @@
<?php
/**
* Print the world page.
*/
function world_controller_get()
{
auth_only_and_must_have_character();
$GLOBALS['active_nav_tab'] = 'world';
echo page('world/base');
}
/**
* Handle a request to move a character.
*/
function world_controller_move_post()
{
/*
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
$d = (int) $_POST['direction'] ?? -1;
// Update the character's position
// 0 = up, 1 = down, 2 = left, 3 = right
$x = location('x');
$y = location('y');
if (isset(directions[$d])) {
$x += directions[$d][0];
$y += directions[$d][1];
} else {
router_error(999);
}
$r = db_query(db_live(), 'UPDATE char_locations SET x = :x, y = :y WHERE char_id = :c', [
':x' => $x,
':y' => $y,
':c' => user('char_id')
]);
if ($r === false) throw new Exception('Failed to move character. (wcmp)');
json_response(['x' => $x, 'y' => $y]);
}

View File

@ -59,12 +59,7 @@ function csrf()
*/
function csrf_verify($token)
{
if (hash_equals($_SESSION['csrf'] ?? '', $token)) {
$_SESSION['csrf'] = token();
return true;
}
return false;
return hash_equals($_SESSION['csrf'] ?? '', $token);
}
/**
@ -185,6 +180,25 @@ function wallet($field = '')
return $GLOBALS['wallet'][$field] ?? false;
}
/**
* Access the character location. On first execution it will populate $GLOBALS['location'] with the location data. This
* way the data is up to date with every request without having to query the database every use within, for example, a
* template. Will return false if the field does not exist, or the entire location array if no field is specified.
*/
function location($field = '')
{
if (empty($GLOBALS['location'])) {
$GLOBALS['location'] = db_query(
db_live(),
"SELECT * FROM char_locations WHERE char_id = :c",
[':c' => user('char_id')]
)->fetchArray(SQLITE3_ASSOC);
}
if ($field === '') return $GLOBALS['location'];
return $GLOBALS['location'][$field] ?? false;
}
/**
* Format an array of strings to a ul element.
*/
@ -231,3 +245,37 @@ function ce($condition, $value, $or = '')
{
echo $condition ? $value : $or;
}
/**
* Get whether the request is an HTMX request.
*/
function is_htmx()
{
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
define('DBP', SRC . '/../database');
/**
* Open a connection to a database.
*/
@ -22,7 +24,7 @@ function db_open($path)
*/
function db_auth()
{
return $GLOBALS['db_auth'] ??= db_open(__DIR__ . '/../database/auth.db');
return $GLOBALS['db_auth'] ??= db_open(DBP . '/auth.db');
}
/**
@ -30,7 +32,7 @@ function db_auth()
*/
function db_live()
{
return $GLOBALS['db_live'] ??= db_open(__DIR__ . '/../database/live.db');
return $GLOBALS['db_live'] ??= db_open(DBP . '/live.db');
}
@ -39,7 +41,7 @@ function db_live()
*/
function db_fights()
{
return $GLOBALS['db_fights'] ??= db_open(__DIR__ . '/../database/fights.db');
return $GLOBALS['db_fights'] ??= db_open(DBP . '/fights.db');
}
@ -48,7 +50,7 @@ function db_fights()
*/
function db_blueprints()
{
return $GLOBALS['db_blueprints'] ??= db_open(__DIR__ . '/../database/blueprints.db');
return $GLOBALS['db_blueprints'] ??= db_open(DBP . '/blueprints.db');
}
/**

View File

@ -5,7 +5,7 @@
*/
function template($name)
{
return __DIR__ . "/../templates/$name.php";
return SRC . "/../templates/$name.php";
}
/**

View File

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

View File

@ -1,4 +1,4 @@
<div id="debug-query-log">
<div class="debug-query-log">
<h3>Query Log</h3>
<p class="mb-2"><?= $GLOBALS['queries'] ?> queries were executed.</p>
<?php

View File

@ -1,4 +1,4 @@
<div id="debug-query-log">
<div class="debug-query-log">
<h3>Stopwatches</h3>
<p class="mb-2">Page execution took <?= number_format((microtime(true) - START_TIME), 10) ?> seconds.</p>
<p>Bootstrap: <?= stopwatch_get('bootstrap') ?> seconds</p>

View File

@ -1,34 +0,0 @@
<canvas></canvas>
<script>
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800;
canvas.height = 600;
const tile_height = 32;
const tile_width = 32;
const map = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
// render the map
map.forEach((tile, index) => {
const x = (index % 10) * tile_width;
const y = Math.floor(index / 10) * tile_height;
ctx.fillStyle = tile === 0 ? 'black' : 'white';
ctx.fillRect(x, y, tile_width, tile_height);
});
</script>

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dragon Knight</title>
<link rel="stylesheet" href="/assets/css/dragon.css">
<script src="/assets/scripts/htmx.js"></script>
</head>
<body>
<header>

View File

@ -1,3 +1,163 @@
<h1>World</h1>
<p>Use WASD keys to move the character</p>
<p>Current location: <span id="char_x"><?= location('x') ?></span>, <span id="char_y"><?= location('y') ?></span></p>
<?= render('components/world_map') ?>
<div id="canvas-container">
<canvas id="canvas"></canvas>
</div>
<script>
const game = {
canvas: document.getElementById('canvas'),
csrf: '<?= csrf() ?>',
tiles: {
size: 32,
colors: {
0: '#fff',
1: '#f00'
}
}
}
const map = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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]
]
let loc_span = {
x: document.getElementById('char_x'),
y: document.getElementById('char_y')
}
let player = { x: <?= location('x') ?>, y: <?= location('y') ?> }
let camera = { x: 0, y: 0 }
let visible = { x: 0, y: 0 }
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 }
// 0 = up, 1 = down, 2 = left, 3 = right
direction = {
'w': 0,
's': 1,
'a': 2,
'd': 3,
'ArrowUp': 0,
'ArrowDown': 1,
'ArrowLeft': 2,
'ArrowRight': 3
}[e.key];
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>