Move CSS around, add render loop

This commit is contained in:
Sky Johnson 2024-10-12 18:39:28 -05:00
parent 17f06fd1d8
commit a6e959b8ef
9 changed files with 629 additions and 591 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,69 @@
.ui.button {
cursor: pointer;
display: inline-block;
border: none;
font-size: 1rem;
background: #f7f8fa linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
color: #111111;
padding: 0.5rem 1rem 0.5rem;
text-align: center;
border-radius: 3px;
user-select: none;
text-decoration: none;
transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease;
-webkit-tap-highlight-color: transparent;
&:hover {
background-color: #e0e0e0;
background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
color: rgba(0, 0, 0, 0.8);
}
&.badge {
font-size: 10px;
padding: 0.1rem 0.25rem;
}
&.primary {
background-color: #f4cc67;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
color: #111111;
border: 1px solid;
border-color: #C59F43 #AA8326 #957321;
&:hover {
background-color: #fac847;
border-color: #C59F43 #AA8326 #957321;
}
}
&.secondary {
background-color: #444c55;
color: #ffffff;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
&:hover {
background-color: #4e5964;
border-color: #32373E #24282D #212429;
}
}
&.danger {
background-color: #e57373;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(139, 0, 0, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
border: 1px solid;
border-color: #d32f2f #c62828 #b71c1c;
&:hover {
background-color: #d95c5c;
border-color: #b71c1c #a52727 #8e1f1f;
}
}
}

View File

@ -0,0 +1,450 @@
@import 'utilities.css';
@import 'buttons.css';
@import 'forms.css';
body {
background-color: #bcc6cf;
background-image: url('/assets/img/bg.jpg');
background-attachment: fixed;
background-position: center top;
background-repeat: no-repeat;
max-width: 1640px;
margin: 0px auto;
font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
}
header {
height: 76px;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
background-image: url('/assets/img/header.jpg');
h1 {
margin: 0;
padding: 0;
}
.right {
display: flex;
align-items: center;
p {
margin-right: 1rem;
}
}
}
main {
padding: 1rem;
width: 100%;
display: flex;
gap: 2rem;
#center {
flex: 1;
}
}
aside {
min-width: 200px;
.box {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 0.15rem;
padding: 0.5rem;
}
}
aside#left nav {
& > *:not(:last-child) {
margin-bottom: 0.25rem;
}
div.stack {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 0.15rem;
input[type="checkbox"] {
display: none;
&:checked ~ div.list {
display: block;
}
&:checked + label {
background-color: rgba(0, 0, 0, 0.5);
color: white;
}
}
label {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 0.15rem;
text-decoration: none;
color: black;
transition: color, background-color 0.2s ease;
cursor: pointer;
img {
height: 18px;
margin-right: 0.25rem;
}
span.text {
display: block;
width: 100%;
}
&:hover {
color: white;
background-color: rgba(0, 0, 0, 0.3);
}
span.arrow {
position: relative;
top: 5px;
}
}
div.list {
display: none;
& > a {
display: block;
width: 100%;
padding: 0.5rem 1rem 0.5rem 1.35rem;
border-radius: 0.15rem;
text-decoration: none;
color: black;
transition: color, background-color 0.2s ease;
&:not(:last-child)::before {
content: '├';
display: inline-block;
margin-right: 0.25rem;
}
&:last-child::before {
content: '└';
display: inline-block;
position: relative;
top: 3px;
margin-right: 0.25rem;
}
&:hover {
background-color: rgba(0, 0, 0, 0.3);
}
&.active {
background-color: #444c55;
color: #ffffff;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
}
}
}
& > a {
display: block;
width: 100%;
padding: 0.5rem 1rem;
text-decoration: none;
color: black;
transition: color, background-color 0.2s ease;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 0.15rem;
&:has(img) {
display: flex;
align-items: center;
img {
height: 18px;
margin-right: 0.25rem;
}
}
&:hover, &.active {
color: white;
}
&:hover {
background-color: rgba(0, 0, 0, 0.3);
}
&.active {
background-color: #444c55;
color: #ffffff;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
border-color: #3D444C #2F353B #2C3137;
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
}
}
}
footer {
display: flex;
justify-content: center;
align-items: center;
margin: 1rem 0;
padding: 1rem;
text-align: center;
color: #666;
& > p:not(:last-child) {
margin-right: 2rem;
}
}
#char-bar {
display: flex;
align-items: center;
justify-content: space-around;
padding: 0 1rem;
height: 34px;
color: white;
gap: 1rem;
background-image: url('/assets/img/bar.jpg');
& > div.container {
display: flex;
align-items: center;
gap: 1rem;
& > div {
display: flex;
align-items: center;
.icon {
width: 18px;
margin-right: 0.5rem;
}
}
}
}
span.badge {
font-size: 10px;
background-color: #f7f8fa;
color: #111111;
border-radius: 0.25rem;
padding: 0.1rem 0.25rem;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1) inset;
&.dark {
background-color: #444c55;
color: white;
}
&.green {
background-color: #a6e3a1;
}
}
.char-meter {
background-color: black;
height: 16px;
min-width: 100px;
border-radius: 0.1rem;
position: relative;
& > div {
height: 100%;
border-radius: 0.1rem;
overflow: hidden;
&.hp {
background-color: #e57373;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(139, 0, 0, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
border: 1px solid;
border-color: #d32f2f #c62828 #b71c1c;
}
&.mp {
background-color: #5a9bd4;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(60, 100, 150, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
border: 1px solid;
border-color: #4a8ab0 #3a7a9c #2a6a88;
}
&.tp {
background-color: #f4cc67;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
border: 1px solid;
border-color: #C59F43 #AA8326 #957321;
}
}
}
.tooltip {
background-color: black;
color: white;
border: 1px solid #666;
font-size: 14px;
padding: 0.5rem;
box-shadow: 0 0 0.5rem 0.1rem rgba(0, 0, 0, 0.2);
border-radius: 0.1rem;
text-align: center;
}
.tooltip-trigger {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.debug-query-log {
padding: 2rem;
font-size: 14px;
color: #666;
font-family: monospace;
}
#center section {
&:not(:last-child) {
padding-bottom: 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin-bottom: 1rem;
}
}
h1:has(.badge), h2:has(.badge), h3:has(.badge), h4:has(.badge), h5:has(.badge), h6:has(.badge) {
display: flex;
align-items: center;
& > .badge {
margin-left: 0.5rem;
}
}
.alert {
position: relative;
min-height: 1rem;
margin: 1rem 0;
background: #f8f8f9;
padding: 0.5rem 1rem;
line-height: 1.4285rem;
color: rgba(0, 0, 0, .87);
transition: opacity .1s ease, color .1s ease, background .1s ease, box-shadow .1s ease;
border-radius: .28571429rem;
box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent;
display: flex;
align-items: center;
justify-content: space-between;
&.success {
background-color: #f0f9eb;
color: #2c662d;
border-color: #b3dc9d;
}
&.danger {
background-color: #f9e9eb;
color: #9f3a38;
border-color: #e0b4b4;
}
&.warning {
background-color: #fff8e1;
color: #573a08;
border-color: #f9e79f;
}
&.info {
background-color: #f0f9fb;
color: #2c7fba;
border-color: #b3d7f9;
}
&.dark {
background-color: #f0f0f0;
color: #2c2c2c;
border-color: #b3b3b3;
}
a[alert-close] {
text-decoration: none;
cursor: pointer;
font-size: 2rem;
color: inherit;
}
}
a {
color: #4C0515;
text-decoration: none;
transition: color 0.2s ease;
&:hover {
color: #6C0515;
text-decoration: underline;
}
}
body::-webkit-scrollbar {
width: 0.5rem;
}
body::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
body::-webkit-scrollbar-thumb {
background-color: #444c55;
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
border: 1px solid;
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;
image-rendering: -webkit-optimize-contrast;
}
}
.char-icon {
width: 32px;
height: 32px;
background-image: url('/assets/img/world/rogues.png');
&.index-0 {
background-position: 0 0;
}
&.index-1 {
background-position: -32px 0;
}
&.index-2 {
background-position: -64px 0;
}
&.index-3 {
background-position: -96px 0;
}
&.index-4 {
background-position: -128px 0;
}
}

View File

@ -0,0 +1,40 @@
:root {
font-size: 16px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; }
.my-2 { margin-bottom: 0.5rem; margin-top: 0.5rem; }
.my-3 { margin-bottom: 0.75rem; margin-top: 0.75rem; }
.my-4 { margin-bottom: 1rem; margin-top: 1rem; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.ml-3 { margin-left: 0.75rem; }
.ml-4 { margin-left: 1rem; }
.mr-1 { margin-right: 0.25rem; }
.mr-2 { margin-right: 0.5rem; }
.mr-3 { margin-right: 0.75rem; }
.mr-4 { margin-right: 1rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.container-960 {
width: 960px;
margin: 0 auto;
}

View File

@ -104,11 +104,11 @@ function user($field = '')
} }
/** /**
* Check whether the user has selected a character. If so, return the character's ID. * Check whether the user has selected a character.
*/ */
function user_selected_char(): int function user_selected_char()
{ {
return (int) $_SESSION['user']['char_id']; return user('char_id') > 0 ? true : false;
} }
/** /**
@ -124,7 +124,7 @@ function char($field = '')
$GLOBALS['char'] = db_query( $GLOBALS['char'] = db_query(
db_live(), db_live(),
"SELECT * FROM characters WHERE id = :c", "SELECT * FROM characters WHERE id = :c",
[':c' => user_selected_char()] [':c' => user('char_id')]
)->fetchArray(SQLITE3_ASSOC); )->fetchArray(SQLITE3_ASSOC);
} }

View File

@ -28,7 +28,7 @@
<main> <main>
<aside id="left"> <aside id="left">
<?php if (user()) echo c_left_nav($activeTab ?? 0); ?> <?php if (user() && user_selected_char()) echo c_left_nav($activeTab ?? 0); ?>
</aside> </aside>
<div id="center"> <div id="center">
@ -36,7 +36,7 @@
</div> </div>
<aside id="right"> <aside id="right">
<?php if (user()): ?> <?php if (user() && user_selected_char()): ?>
<div class="box"> <div class="box">
@TODO @TODO
</div> </div>

View File

@ -3,6 +3,6 @@
<a href="/auth/login" class="ui button primary">Login</a> <a href="/auth/login" class="ui button primary">Login</a>
<a href="/auth/register" class="ui button secondary">Register</a> <a href="/auth/register" class="ui button secondary">Register</a>
<?php else: ?> <?php else: ?>
<h1 class="tooltip-click" data-tooltip-content="Hover-based tooltip">Home</h1> <h1>Home</h1>
<?= print_r(char()) ?> <p>Welcome, <?= user('username') ?>!</p>
<?php endif; ?> <?php endif; ?>

View File

@ -55,13 +55,25 @@
y: document.getElementById('char_y') y: document.getElementById('char_y')
} }
let player = { x: <?= location('x') ?>, y: <?= location('y') ?>, char: 23, sprite: { x: 0, y: 0 } } let player = {
location: { x: <?= location('x') ?>, y: <?= location('y') ?> },
current: { x: <?= location('x') ?>, y: <?= location('y') ?> },
target: { x: <?= location('x') ?>, y: <?= location('y') ?> },
char: 23, sprite: { x: 0, y: 0 },
tweenDuration: 0.2, // seconds
tweenProgress: 0
}
let camera = { x: 0, y: 0 } let camera = { x: 0, y: 0 }
let visible = { x: 0, y: 0 } let visible = { x: 0, y: 0 }
game.tiles.img.src = '/assets/img/world/tiles.jpg'; game.tiles.img.src = '/assets/img/world/tiles.jpg';
game.sprites.img.src = '/assets/img/world/rogues.png'; game.sprites.img.src = '/assets/img/world/rogues.png';
let lastFrameTime = 0;
let fps = 0;
let debounce = false;
function getPlayerSprite() { function getPlayerSprite() {
let col = player.char % game.sprites.cols let col = player.char % game.sprites.cols
let row = Math.floor(player.char / game.sprites.cols) let row = Math.floor(player.char / game.sprites.cols)
@ -73,17 +85,18 @@
game.canvas.height = game.canvas.clientHeight game.canvas.height = game.canvas.clientHeight
visible.x = Math.ceil(game.canvas.width / game.tiles.size) visible.x = Math.ceil(game.canvas.width / game.tiles.size)
visible.y = Math.ceil(game.canvas.height / game.tiles.size) visible.y = Math.ceil(game.canvas.height / game.tiles.size)
updateCamera()
render()
} }
function setupEventListeners() { function setupEventListeners() {
window.addEventListener('resize', updateCanvasSize) window.addEventListener('resize', updateCanvasSize)
window.addEventListener('keydown', handleKeyPress) window.addEventListener('keydown', handleKeyPress)
window.addEventListener('keyup', () => debounce = false)
} }
// Handle keyboard input // Handle keyboard input
function handleKeyPress(e) { function handleKeyPress(e) {
if (debounce) return;
debounce = true;
let moved = false; let moved = false;
const newPos = { ...player } const newPos = { ...player }
@ -104,8 +117,8 @@
const dy = [-1, 1, 0, 0]; const dy = [-1, 1, 0, 0];
// Calculate new position // Calculate new position
const newX = player.x + dx[direction]; const newX = player.location.x + dx[direction];
const newY = player.y + dy[direction]; const newY = player.location.y + dy[direction];
if (direction !== undefined) { if (direction !== undefined) {
// Check if the new position is outside the map bounds // Check if the new position is outside the map bounds
@ -124,12 +137,11 @@
}).then(response => { }).then(response => {
if (response.ok) { if (response.ok) {
response.json().then(data => { response.json().then(data => {
player.x = data.x player.location = { x: data.x, y: data.y }
player.y = data.y player.target = { x: data.x, y: data.y }
loc_span.x.textContent = player.x player.tweenProgress = 0
loc_span.y.textContent = player.y loc_span.x.textContent = player.location.x
updateCamera() loc_span.y.textContent = player.location.y
render()
}); });
} else { } else {
throw new Error('Failed to move character'); throw new Error('Failed to move character');
@ -140,8 +152,8 @@
// Update camera position // Update camera position
function updateCamera() { function updateCamera() {
camera.x = player.x * game.tiles.size - canvas.width / 2; camera.x = player.current.x * game.tiles.size - canvas.width / 2;
camera.y = player.y * game.tiles.size - canvas.height / 2; camera.y = player.current.y * game.tiles.size - canvas.height / 2;
// Clamp camera to map bounds // Clamp camera to map bounds
camera.x = Math.max(0, Math.min(camera.x, camera.x = Math.max(0, Math.min(camera.x,
@ -150,10 +162,21 @@
map.length * game.tiles.size - canvas.height)); map.length * game.tiles.size - canvas.height));
} }
function lerp(start, end, t) {
return start + (end - start) * t;
}
// Render the game // Render the game
function render() { function render(t) {
const ctx = game.canvas.getContext('2d') const ctx = game.canvas.getContext('2d')
// Calculate FPS
if (lastFrameTime) {
const delta = (t - lastFrameTime) / 1000;
fps = Math.round(1 / delta);
}
lastFrameTime = t;
ctx.clearRect(0, 0, game.canvas.width, game.canvas.height) ctx.clearRect(0, 0, game.canvas.width, game.canvas.height)
// Calculate visible tile range // Calculate visible tile range
@ -169,24 +192,43 @@
for (let x = startTileX; x < endTileX; x++) { for (let x = startTileX; x < endTileX; x++) {
if (x >= map[0].length) continue if (x >= map[0].length) continue
const screenX = x * game.tiles.size - camera.x const screenX = Math.round(x * game.tiles.size - camera.x)
const screenY = y * game.tiles.size - camera.y const screenY = Math.round(y * game.tiles.size - camera.y)
ctx.drawImage(game.tiles.img, map[y][x] * game.tiles.size, 0, game.tiles.size, game.tiles.size, ctx.drawImage(game.tiles.img, map[y][x] * game.tiles.size, 0, game.tiles.size, game.tiles.size,
screenX, screenY, game.tiles.size, game.tiles.size) screenX, screenY, game.tiles.size, game.tiles.size)
} }
} }
// Tween player position
if (player.tweenProgress < 1) {
player.tweenProgress += 1 / player.tweenDuration / 60
player.current.x = lerp(player.current.x, player.target.x, player.tweenProgress)
player.current.y = lerp(player.current.y, player.target.y, player.tweenProgress)
} else {
player.current = { x: player.current.x, y: player.current.y }
}
updateCamera()
// Render the player on top of the map using their current position // Render the player on top of the map using their current position
const playerX = Math.round((player.current.x * game.tiles.size) - camera.x)
const playerY = Math.round((player.current.y * game.tiles.size) - camera.y)
ctx.drawImage(game.sprites.img, player.sprite.x, player.sprite.y, game.sprites.size, game.sprites.size, ctx.drawImage(game.sprites.img, player.sprite.x, player.sprite.y, game.sprites.size, game.sprites.size,
(player.x * game.sprites.size) - camera.x, (player.y * game.sprites.size) - camera.y, playerX, playerY, game.sprites.size, game.sprites.size)
game.sprites.size, game.sprites.size)
// Render FPS counter
ctx.fillStyle = 'white';
ctx.font = '16px Arial';
ctx.fillText(`FPS: ${fps}`, game.canvas.width - 70, 20);
requestAnimationFrame(render);
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
getPlayerSprite() getPlayerSprite()
updateCanvasSize() updateCanvasSize()
setupEventListeners() setupEventListeners()
render() requestAnimationFrame(render)
}) })
</script> </script>