diff --git a/public/assets/css/basic.css b/public/assets/css/basic.css new file mode 100644 index 0000000..00df23d --- /dev/null +++ b/public/assets/css/basic.css @@ -0,0 +1,191 @@ +@import 'utilities.css'; +@import 'buttons.css'; +@import 'forms.css'; + +body { + font-family: var(--main-font); + background-color: #bcc6cf; + background-image: url('/assets/img/bg.jpg'); + background-attachment: fixed; + background-position: center top; + background-repeat: no-repeat; + width: 100vw; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +header#basic-header { + display: flex; + justify-content: center; + margin: 1rem 0; +} + +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; + } +} + +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; + } +} + +.tooltip { + position: absolute; + 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; +} + +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; +} + +.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; + } +} diff --git a/public/assets/css/build.sh b/public/assets/css/build.sh deleted file mode 100755 index 4868e8b..0000000 --- a/public/assets/css/build.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -if ! command -v bun &> /dev/null -then - echo "Bun is not installed. Please install it from https://bun.sh" - exit 1 -fi - -if ! command -v entr &> /dev/null -then - echo "entr is not installed. Installing entr..." - # For Debian/Ubuntu-based systems - if [[ -x "$(command -v apt)" ]]; then - sudo apt update && sudo apt install entr -y - # For Red Hat-based systems - elif [[ -x "$(command -v yum)" ]]; then - sudo yum install entr -y - # For macOS with Homebrew - elif [[ -x "$(command -v brew)" ]]; then - brew install entr - else - echo "Package manager not supported. Please install entr manually." - exit 1 - fi -fi - -echo "Running 'find src/ | entr -s \"bunx lightningcss-cli --minify --bundle src/main.css -o dragon.css\"'..." -find src/ | entr -s 'bunx lightningcss-cli --minify --bundle src/main.css -o dragon.css' diff --git a/public/assets/css/src/buttons.css b/public/assets/css/buttons.css similarity index 100% rename from public/assets/css/src/buttons.css rename to public/assets/css/buttons.css diff --git a/public/assets/css/dragon.css b/public/assets/css/dragon.css deleted file mode 100644 index dd235f1..0000000 --- a/public/assets/css/dragon.css +++ /dev/null @@ -1 +0,0 @@ -:root{--main-font:Cambria,Cochin,Georgia,Times,"Times New Roman",serif;font-size:16px}*{box-sizing:border-box;margin:0;padding:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.container-960{width:960px;margin:0 auto}.ui.button{cursor:pointer;font-size:1rem;font-family:var(--main-font);color:#111;text-align:center;user-select:none;-webkit-tap-highlight-color:transparent;background:#f7f8fa linear-gradient(#fff0,#0000001a);border:none;border-radius:3px;padding:.5rem 1rem;text-decoration:none;transition:opacity .1s,background-color .1s,color .1s,background .1s;display:inline-block;box-shadow:inset 0 1px 0 1px #ffffff4d,inset 0 0 0 1px #adb2bb;&:hover{color:#000c;background-color:#e0e0e0;background-image:linear-gradient(#fff0,#0000001a);box-shadow:inset 0 1px 0 1px #ffffff4d,inset 0 0 0 1px #adb2bb}&.badge{padding:.1rem .25rem;font-size:10px}&.primary{color:#111;background-color:#f4cc67;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #aa8326;border-color:#c59f43 #aa8326 #957321;box-shadow:inset 0 1px #fff3;&:hover{background-color:#fac847;border-color:#c59f43 #aa8326 #957321}}&.secondary{color:#fff;background-color:#444c55;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #2f353b;border-color:#3d444c #2f353b #2c3137;box-shadow:inset 0 1px #fff3;&:hover{background-color:#4e5964;border-color:#32373e #24282d #212429}}&.danger{background-color:#e57373;background-image:linear-gradient(#ffffff26,#8b00001a);border:1px solid #c62828;border-color:#d32f2f #c62828 #b71c1c;box-shadow:inset 0 1px #fff3;&:hover{background-color:#d95c5c;border-color:#b71c1c #a52727 #8e1f1f}}}.form.control{appearance:none;color:#fff;background-color:#0003;border:1px solid #0000;border-radius:4px;outline:none;width:100%;padding:.5rem;font-size:1rem;display:block;box-shadow:inset 0 1px 4px #0000001a;&::placeholder{color:#ffffffb3}&:hover{background-color:#0000004d}&:focus{background-color:#00000080;border-color:#000c}&.error{background-color:#ff2b2b33;&:hover{background-color:#ff2b2b4d}&:focus{background-color:#ff2b2b4d;border-color:#ff2b2bcc}}}.form.group{margin-bottom:1rem;&>label{margin-bottom:.5rem;display:block}&>.form.control:not(:last-child){margin-bottom:.5rem}}.character-select>.radio-block{background-color:#0003;border-radius:.15rem;display:inline-block;&:not(:last-child){margin-bottom:.25rem}&>input[type=radio]{display:none}&>label{cursor:pointer;background-image:linear-gradient(#fff0,#0000);border:1px solid #0000;border-radius:.15rem;align-items:center;width:100%;padding:.5rem;transition:color,background-color,border-color,background-image .2s;display:flex;&:hover{color:#fff;background-color:#0000004d}&>.badge{margin-left:.25rem}&>span.selected{display:none}&>.char-icon{margin-right:.25rem}}&.active>label{color:#fff;background-color:#444c55;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #2f353b;border-color:#3d444c #2f353b #2c3137;box-shadow:inset 0 1px #fff3;&>span.selected{display:inline-block}}&>input[type=radio]:checked+label{color:#111;background-color:#f4cc67;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #aa8326;border-color:#c59f43 #aa8326 #957321;box-shadow:inset 0 1px #fff3}&>input[type=radio]:disabled+label{cursor:default}}.character-select:not(:has(input[type=radio]:checked))>.buttons{display:none}section.profile{& header{text-align:center;margin-bottom:2rem;& h3{color:#0000004d;text-transform:uppercase;font-size:1rem}& h4{font-size:.75rem}}&>div.grid{gap:1rem;display:flex;&>section{width:50%;&>div:not(:last-child){margin-bottom:1rem}}}& div.avatar{justify-content:center;align-items:center;display:flex;& img{max-width:250px}}& h4{text-align:center;text-transform:uppercase;color:#fff;background-image:url(/assets/img/bar.jpg);background-position:bottom;margin-bottom:.5rem;padding:.5rem;font-size:.75rem}& div.stats{&>.grid{grid-template-columns:1fr 1fr;gap:.25rem;display:grid;&>div.cell{justify-content:space-between;align-items:center;padding:.25rem .5rem;display:flex;& .label{text-transform:uppercase;margin-right:.25rem;font-size:.75rem}}}}}main#game-container{width:100vw;height:100vh}canvas#game-canvas{width:100%;height:100%}body{min-width:968px;max-width:1640px;font-family:var(--main-font);background-color:#bcc6cf;background-image:url(/assets/img/bg.jpg);background-position:top;background-repeat:no-repeat;background-attachment:fixed;margin:0 auto}header#main-header{color:#fff;background-image:url(/assets/img/header.jpg);justify-content:space-between;align-items:center;height:76px;padding:0 1rem;display:flex;& h1{margin:0;padding:0}& .right{align-items:center;display:flex;& p{margin-right:1rem}}}main{gap:2rem;width:100%;padding:1rem;display:flex;& #center{flex:1}}aside{min-width:200px;& .box{background-color:#0003;border-radius:.15rem;padding:.5rem}}aside#left nav{&>:not(:last-child){margin-bottom:.25rem}& div.stack{background-color:#0003;border-radius:.15rem;& input[type=checkbox]{display:none;&:checked~div.list{display:block}&:checked+label{color:#fff;background-color:#00000080}}& label{color:#000;cursor:pointer;border-radius:.15rem;align-items:center;padding:.5rem 1rem;text-decoration:none;transition:color,background-color .2s;display:flex;& img{height:18px;margin-right:.25rem}& span.text{width:100%;display:block}&:hover{color:#fff;background-color:#0000004d}& span.arrow{position:relative;top:5px}}& div.list{display:none;&>a{color:#000;border-radius:.15rem;width:100%;padding:.5rem 1rem .5rem 1.35rem;text-decoration:none;transition:color,background-color .2s;display:block;&:not(:last-child):before{content:"├";margin-right:.25rem;display:inline-block}&:last-child:before{content:"└";margin-right:.25rem;display:inline-block;position:relative;top:3px}&:hover{background-color:#0000004d}&.active{color:#fff;background-color:#444c55;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #2f353b;border-color:#3d444c #2f353b #2c3137;box-shadow:inset 0 1px #fff3}}}}&>a{color:#000;background-color:#0003;border-radius:.15rem;width:100%;padding:.5rem 1rem;text-decoration:none;transition:color,background-color .2s;display:block;&:has(img){align-items:center;display:flex;& img{height:18px;margin-right:.25rem}}&:hover,&.active{color:#fff}&:hover{background-color:#0000004d}&.active{color:#fff;background-color:#444c55;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #2f353b;border-color:#3d444c #2f353b #2c3137;box-shadow:inset 0 1px #fff3}}}footer{text-align:center;color:#666;justify-content:center;align-items:center;margin:1rem 0;padding:1rem;display:flex;&>p:not(:last-child){margin-right:2rem}}#character{&>.name{align-items:center;display:flex}&>div:not(:last-child){margin-bottom:.5rem}}span.badge{color:#111;background-color:#f7f8fa;border-radius:.25rem;padding:.1rem .25rem;font-size:10px;box-shadow:inset 0 0 0 1px #0000001a;&.dark{color:#fff;background-color:#444c55}&.green{background-color:#a6e3a1}}.char-meter{background-color:#000;border-radius:.1rem;min-width:100px;height:16px;position:relative;&>div{border-radius:.1rem;height:100%;overflow:hidden;&.hp{background-color:#e57373;background-image:linear-gradient(#ffffff26,#8b00001a);border:1px solid #c62828;border-color:#d32f2f #c62828 #b71c1c;box-shadow:inset 0 1px #fff3}&.mp{background-color:#5a9bd4;background-image:linear-gradient(#ffffff26,#3c64961a);border:1px solid #3a7a9c;border-color:#4a8ab0 #3a7a9c #2a6a88;box-shadow:inset 0 1px #fff3}&.tp{background-color:#f4cc67;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #aa8326;border-color:#c59f43 #aa8326 #957321;box-shadow:inset 0 1px #fff3}}}.tooltip{color:#fff;text-align:center;background-color:#000;border:1px solid #666;border-radius:.1rem;padding:.5rem;font-size:14px;box-shadow:0 0 .5rem .1rem #0003}.tooltip-trigger{width:100%;height:100%;position:absolute;top:0;left:0}.debug-query-log{color:#666;padding:1rem;font-family:monospace;font-size:14px;&:last-child{padding-top:0}}#center>section{&:not(:last-child){border-bottom:1px solid #0000001a;margin-bottom:1rem;padding-bottom:1rem}}h1:has(.badge),h2:has(.badge),h3:has(.badge),h4:has(.badge),h5:has(.badge),h6:has(.badge){align-items:center;display:flex;&>.badge{margin-left:.5rem}}.alert{color:#000000de;background:#f8f8f9;border-radius:.285714rem;justify-content:space-between;align-items:center;min-height:1rem;margin:1rem 0;padding:.5rem 1rem;line-height:1.4285rem;transition:opacity .1s,color .1s,background .1s,box-shadow .1s;display:flex;position:relative;box-shadow:inset 0 0 0 1px #22242638,0 0 #0000;&.success{color:#2c662d;background-color:#f0f9eb;border-color:#b3dc9d}&.danger{color:#9f3a38;background-color:#f9e9eb;border-color:#e0b4b4}&.warning{color:#573a08;background-color:#fff8e1;border-color:#f9e79f}&.info{color:#2c7fba;background-color:#f0f9fb;border-color:#b3d7f9}&.dark{color:#2c2c2c;background-color:#f0f0f0;border-color:#b3b3b3}& a[alert-close]{cursor:pointer;color:inherit;font-size:2rem;text-decoration:none}}a{color:#4c0515;text-decoration:none;transition:color .2s;&:hover{color:#6c0515;text-decoration:underline}}body::-webkit-scrollbar{width:.5rem}body::-webkit-scrollbar-track{background:#0000001a}body::-webkit-scrollbar-thumb{background-color:#444c55;background-image:linear-gradient(#ffffff26,#0000001a);border:1px solid #2f353b;border-color:#3d444c #2f353b #2c3137;box-shadow:inset 0 1px #fff3}#canvas-container{&>canvas{width:100%;height:440px;image-rendering:pixelated;image-rendering:crisp-edges;image-rendering:-webkit-optimize-contrast;display:block}}.char-icon{background-image:url(/assets/img/world/rogues.png);width:32px;height:32px;&.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}} diff --git a/public/assets/css/src/forms.css b/public/assets/css/forms.css similarity index 100% rename from public/assets/css/src/forms.css rename to public/assets/css/forms.css diff --git a/public/assets/css/game.css b/public/assets/css/game.css index f87e31f..9266295 100644 --- a/public/assets/css/game.css +++ b/public/assets/css/game.css @@ -47,11 +47,8 @@ div#game-windows { & > div.window { pointer-events: auto; background-color: #bcc6cf; - background-image: url('/assets/img/bg.jpg'); - background-attachment: fixed; - background-position: center top; - background-repeat: no-repeat; - box-shadow: 0px 0px 5px black; + background-image: url('/assets/img/ui/bg.webp'); + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); border-radius: 4px; position: absolute; @@ -63,13 +60,15 @@ div#game-windows { display: flex; align-items: center; justify-content: space-between; - padding: 1rem 1rem 0.5rem 1rem; + padding: 0.5rem; cursor: grab; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); .title { font-weight: bold; margin-right: 1rem; user-select: none; + color: rgba(0, 0, 0, 0.5); &:empty { margin-right: 0; @@ -77,15 +76,20 @@ div#game-windows { } .close { - width: 1.5rem; - height: 1.5rem; + width: 16px; + height: 16px; user-select: none; cursor: pointer; + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; } } .body { - padding: 0 1rem 1rem 1rem; + padding: 0.5rem; &:empty { padding: 0; diff --git a/public/assets/css/src/main.css b/public/assets/css/src/main.css deleted file mode 100644 index c943db0..0000000 --- a/public/assets/css/src/main.css +++ /dev/null @@ -1,442 +0,0 @@ -@import 'utilities.css'; -@import 'buttons.css'; -@import 'forms.css'; -@import 'profile.css'; -@import 'game.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; - min-width: 968px; - margin: 0px auto; - font-family: var(--main-font); -} - -header#main-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; - } -} - -#character { - & > .name { - display: flex; - align-items: center; - } - - & > div:not(:last-child) { - margin-bottom: 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 { - position: absolute; - 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: 1rem; - font-size: 14px; - color: #666; - font-family: monospace; - - &:last-child { - padding-top: 0; - } -} - -#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; - } -} diff --git a/public/assets/css/src/profile.css b/public/assets/css/src/profile.css deleted file mode 100644 index b8680e4..0000000 --- a/public/assets/css/src/profile.css +++ /dev/null @@ -1,132 +0,0 @@ -section.profile { - header { - text-align: center; - margin-bottom: 3rem; - - h3 { - font-size: 1rem; - } - - h5 { - color: rgba(0, 0, 0, 0.5); - font-size: 0.75rem; - } - } - - & > div.grid { - display: flex; - gap: 1rem; - - & > section { - width: 50%; - - & > div:not(:last-child) { - margin-bottom: 2rem; - } - } - } - - div.avatar { - display: flex; - align-items: center; - justify-content: center; - padding-bottom: 1rem; - - img { - height: 185px; - width: 185px; - } - - .border { - width: 250px; - height: 250px; - position: absolute; - } - } - - h4 { - text-align: center; - text-transform: uppercase; - color: white; - font-size: 0.75rem; - margin-bottom: 0.5rem; - background-image: url('/assets/img/bar.jpg'); - background-position: bottom center; - padding: 0.5rem; - } - - div.stats { - & > .grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.25rem; - - & > div.cell { - padding: 0.25rem 0.5rem; - display: flex; - align-items: center; - justify-content: space-between; - - .label { - font-size: 0.75rem; - text-transform: uppercase; - margin-right: 0.25rem; - } - } - } - } - - #equipped-gear { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.5rem; - - div.item { - display: flex; - justify-content: center; - align-items: center; - - &.i-1x1 { - width: 30px; - height: 30px; - background-image: url('/assets/img/ui/1x1.png'); - } - - &.i-2x2 { - width: 60px; - height: 60px; - background-image: url('/assets/img/ui/2x2.png'); - } - - &.i-2x3 { - width: 60px; - height: 90px; - background-image: url('/assets/img/ui/2x3.png'); - } - } - - & > div { - display: flex; - gap: 0.5rem; - - & > div { - display: flex; - align-items: center; - justify-content: center; - width: 60px; - - &.top, &.bot { - width: 60px; - height: 60px; - } - - &.mid { - width: 60px; - height: 90px; - } - } - } - } -} diff --git a/public/assets/css/src/utilities.css b/public/assets/css/utilities.css similarity index 94% rename from public/assets/css/src/utilities.css rename to public/assets/css/utilities.css index 653019f..9351d82 100644 --- a/public/assets/css/src/utilities.css +++ b/public/assets/css/utilities.css @@ -39,3 +39,8 @@ width: 960px; margin: 0 auto; } + +.container-480 { + width: 480px; + margin: 0 auto; +} diff --git a/public/assets/img/dk.png b/public/assets/img/dk.png new file mode 100644 index 0000000..9d310eb Binary files /dev/null and b/public/assets/img/dk.png differ diff --git a/public/assets/img/ui/bg.webp b/public/assets/img/ui/bg.webp new file mode 100644 index 0000000..bdc5375 Binary files /dev/null and b/public/assets/img/ui/bg.webp differ diff --git a/public/assets/img/ui/icons/bargraph.png b/public/assets/img/ui/icons/bargraph.png new file mode 100644 index 0000000..5de6e97 Binary files /dev/null and b/public/assets/img/ui/icons/bargraph.png differ diff --git a/public/assets/img/ui/icons/beer.png b/public/assets/img/ui/icons/beer.png new file mode 100644 index 0000000..33a8a26 Binary files /dev/null and b/public/assets/img/ui/icons/beer.png differ diff --git a/public/assets/img/ui/icons/bullet_red.png b/public/assets/img/ui/icons/bullet_red.png new file mode 100644 index 0000000..3036084 Binary files /dev/null and b/public/assets/img/ui/icons/bullet_red.png differ diff --git a/public/assets/img/ui/icons/stop.png b/public/assets/img/ui/icons/stop.png new file mode 100644 index 0000000..808fa9d Binary files /dev/null and b/public/assets/img/ui/icons/stop.png differ diff --git a/public/assets/img/ui/icons/user.png b/public/assets/img/ui/icons/user.png new file mode 100644 index 0000000..122dbf5 Binary files /dev/null and b/public/assets/img/ui/icons/user.png differ diff --git a/public/assets/scripts/WindowManager.js b/public/assets/scripts/WindowManager.js index 92dc5c8..f1e19d9 100644 --- a/public/assets/scripts/WindowManager.js +++ b/public/assets/scripts/WindowManager.js @@ -16,10 +16,12 @@ class WindowManager let w = this.windows[id] w.querySelector('header .title').innerHTML = title w.querySelector('.body').innerHTML = content + this.bringToFront(w) return } - this.createWindow(id, content, title) + let w = this.createWindow(id, content, title) + this.bringToFront(w) } createWindow(id, content, title = '') @@ -40,12 +42,8 @@ class WindowManager h.appendChild(ht) // create close button - ht.insertAdjacentHTML('afterend', ` - - `) - h.querySelector('svg').addEventListener('click', () => { + ht.insertAdjacentHTML('afterend', '') + h.querySelector('a.close').addEventListener('click', () => { this.windows[id].remove() delete this.windows[id] }) @@ -60,6 +58,7 @@ class WindowManager this.makeWindowDraggable(w, this.container) this.windows[id] = w this.container.appendChild(w) + return w } makeWindowDraggable(w, c) diff --git a/public/index.php b/public/index.php index e2dfebd..56cc1ca 100644 --- a/public/index.php +++ b/public/index.php @@ -11,73 +11,370 @@ $r = new Router; /* Home */ -$r->get('/', function () { - if (user()) must_have_character(); - $GLOBALS['active_nav_tab'] = 'home'; - echo render('layouts/basic', ['view' => 'pages/home']); +$r->get('/', function() { + if (!user()) redirect('/login'); + redirect('/world'); }); /* Auth */ -$r->get('/auth/register', 'auth_controller_register_get'); -$r->post('/auth/register', 'auth_controller_register_post'); -$r->get('/auth/login', 'auth_controller_login_get'); -$r->post('/auth/login', 'auth_controller_login_post'); -$r->post('/auth/logout', 'auth_controller_logout_post'); +$r->get('/register', function() { + guest_only(); + echo render('layouts/basic', ['view' => 'pages/auth/register']); +}); + +$r->post('/register', function() { + guest_only(); + csrf_ensure(); + + $errors = []; + + $u = trim($_POST['u'] ?? ''); + $e = trim($_POST['e'] ?? ''); + $p = $_POST['p'] ?? ''; + + /* + A username is required. + A username must be at least 3 characters long and at most 18 characters long. + A username must contain only alphanumeric characters and spaces. + */ + if (empty($u) || strlen($u) < 3 || strlen($u) > 18 || !ctype_alnum(str_replace(' ', '', $u))) { + $errors['u'][] = 'Username is required and must be between 3 and 18 characters long and contain only + alphanumeric characters and spaces.'; + } + + /* + An email is required. + An email must be at most 255 characters long. + An email must be a valid email address. + */ + if (empty($e) || strlen($e) > 255 || !filter_var($e, FILTER_VALIDATE_EMAIL)) { + $errors['e'][] = 'Email is required must be a valid email address.'; + } + + /* + A password is required. + A password must be at least 6 characters long. + */ + if (empty($p) || strlen($p) < 6) { + $errors['p'][] = 'Password is required and must be at least 6 characters long.'; + } + + /* + A username must be unique. + */ + if (User::username_exists($u)) { + $errors['u'][] = 'Username is already taken.'; + } + + /* + An email must be unique. + */ + if (User::email_exists($e)) { + $errors['e'][] = 'Email is already taken.'; + } + + // If there are errors at this point, send them to the page with errors flashed. + if (!empty($errors)) { + $GLOBALS['form-errors'] = $errors; + echo page('auth/register'); + exit; + } + + if (User::create($u, $e, $p) === false) error_response(400); + + $_SESSION['user'] = serialize(User::find($u)); + Wallet::create(user()->id); + redirect('/character/create-first'); +}); + +$r->get('/login', function() { + guest_only(); + echo render('layouts/basic', ['view' => 'pages/auth/login']); +}); + +$r->post('/login', function() { + guest_only(); + csrf_ensure(); + + $errors = []; + + $u = trim($_POST['u'] ?? ''); + $p = $_POST['p'] ?? ''; + + if (empty($u)) $errors['u'][] = 'Username is required.'; + if (empty($p)) $errors['p'][] = 'Password is required.'; + + // If there are errors at this point, send them to the page with errors flashed. + if (!empty($errors)) { + $GLOBALS['form-errors'] = $errors; + echo render('layouts/basic', ['view' => 'pages/auth/login']); + exit; + } + + $user = User::find($u); + if ($user === false || !$user->check_password($p)) { + $errors['x'][] = 'Invalid username or password.'; + $GLOBALS['form-errors'] = $errors; + echo render('layouts/basic', ['view' => 'pages/auth/login']); + exit; + } + + $_SESSION['user'] = serialize($user); + + if ($_POST['remember'] ?? false) { + $token = token(); + $expires = strtotime('+30 days'); + $result = db_query( + db_auth(), + "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", + [':t' => $token, ':u' => user()->id, ':e' => $expires] + ); + if (!$result) error_response(400); + set_cookie('remember_me', $token, $expires); + } + + if (user()->char_count() === 0) { + redirect('/character/create-first'); + } elseif (!change_user_character(user()->char_id)) { + echo "failed to change user character (aclp)"; + error_response(999); + } + + redirect('/'); +}); + +$r->post('/logout', function() { + csrf_ensure(); + session_delete(user()->id); + unset($_SESSION['user']); + set_cookie('remember_me', '', 1); + redirect('/'); +}); + +$r->get('/debug/logout', function() { + session_delete(user()->id); + unset($_SESSION['user']); + set_cookie('remember_me', '', 1); + redirect('/'); +}); /* Characters */ -$r->get('/characters', 'char_controller_list_get'); -$r->post('/characters', 'char_controller_list_post'); -$r->get('/character/create-first', 'char_controller_create_first_get'); -$r->post('/character/create', 'char_controller_create_post'); -$r->post('/character/delete', 'char_controller_delete_post'); +$r->get('/characters', function() { + auth_only_and_must_have_character(); + + $GLOBALS['active_nav_tab'] = 'chars'; + echo page('chars/list', ['chars' => user()->char_list()]); +}); + +$r->post('/characters', function() { + auth_only_and_must_have_character(); + csrf_ensure(); + + $GLOBALS['active_nav_tab'] = 'chars'; + + $char_id = (int) ($_POST['char_id'] ?? 0); + $action = $_POST['action'] ?? ''; + + // If the character ID is not a number, or the action is not a string, return a 400. + if (!is_numeric($char_id) || !is_string($action)) error_response(400); + + // If the character ID is 0, return to the list. + if ($char_id === 0) { + flash('alert_character_list_1', ['', 'No character selected.']); + redirect('/characters'); + } + + // If the action is not one of the allowed actions, return a 400. + if (!in_array($action, ['select', 'delete'])) error_response(400); + + // If the action is to select a character, change the user's selected character. + if ($action === 'select') { + // If the character ID is the current character, do nothing. + if ($char_id === user()->char_id || $char_id === 0) { + flash('alert_character_list_1', ['info', 'You are already using ' . char()->name . '.']); + redirect('/characters'); + } + + if (!Character::belongs_to($char_id, user()->id)) error_response(999); + + change_user_character($char_id); + + flash('alert_character_list_1', ['success', 'Switched to ' . char()->name . '!']); + } + + // If the action is to delete a character, move to the confirmation page. + if ($action === 'delete') { + if (!Character::belongs_to($char_id, user()->id)) error_response(999); + + echo page('chars/delete', ['char' => Character::find($char_id)]); + exit; + } + + redirect('/characters'); +}); + +$r->get('/character/create-first', function() { + auth_only(); + + $GLOBALS['active_nav_tab'] = 'chars'; + + // If the user already has a character, redirect them to the main page. + if (user()->char_count() > 0) redirect('/'); + + echo page('chars/first'); +}); + +$r->post('/character/create', function() { + auth_only(); csrf_ensure(); + + $GLOBALS['active_nav_tab'] = 'chars'; + + $errors = []; + + $name = trim($_POST['n'] ?? ''); + + /* + A name is required. + A name must be between 3 and 18 characters. + A name must contain only alphanumeric characters and spaces. + */ + if (empty($name) || strlen($name) < 3 || strlen($name) > 18 || !ctype_alnum(str_replace(' ', '', $name))) { + $errors['n'][] = 'Name is required and must be between 3 and 18 characters long and contain only alphanumeric characters and spaces.'; + } + + /* + A character's name must be unique. + */ + if (Character::name_exists($name)) $errors['n'][] = 'Name is already taken.'; + + // If there are errors at this point, send them to the page with errors flashed. + if (!empty($errors)) { + $GLOBALS['form-errors-create-character'] = $errors; + + if (isset($_POST['first']) && $_POST['first'] === 'true') { + // If this is the first character, return to the first character creation page. + echo page('chars/first'); + exit; + } else { + // If this is not the first character, return to the character list page. + echo page('chars/list', ['chars' => user()->char_list()]); + exit; + } + } + + if (($char = Character::create(user()->id, $name)) === false) error_response(400); + + // Create the auxiliary tables + $char->create_location(); + $char->create_gear(); + + // Award the Adventurer title. + $char->award_title(1); + + // Set the character as the user's selected character + change_user_character($char->id); + + flash('alert_character_list_1', ['success', 'Character ' . $name . ' created!']); + redirect('/characters'); +}); + +$r->post('/character/delete', function() { + auth_only_and_must_have_character(); + csrf_ensure(); + + $char_id = (int) ($_POST['char_id'] ?? 0); + + // If the character ID is not a number, return a 400. + if (!is_numeric($char_id)) error_response(400); + + // Ensure the character ID is valid and belongs to the user. + if (!Character::belongs_to($char_id, user()->id)) error_response(999); + + $char = Character::find($char_id); + + // Confirm the name matches the name of the character. CASE SENSITIVE. + if ($char['name'] !== trim($_POST['n'] ?? '')) { + flash('alert_character_list_1', ['danger', 'Failed to delete ' . $char['name'] . '. Name confirmation did not match.']); + redirect('/characters'); + } + + // Delete the character + Character::delete($char_id); + + // If the character being deleted is the currently selected character, select the first character. + if (user()->char_id === $char_id) { + $chars = user()->char_list(); + if (count($chars) > 0) change_user_character($chars[0]['id']); + } + + flash('alert_character_list_1', ['danger', 'Character ' . $char['name'] . ' deleted.']); + redirect('/characters'); +}); /* World */ -$r->get('/world', 'world_controller_get'); -$r->post('/move', 'world_controller_move_post'); +$r->get('/world', function() { + auth_only_and_must_have_character(); + echo render('layouts/game'); +}); -/* - Profile -*/ -$r->get('/profile', 'profile_controller_get'); -$r->get('/profile/:id', 'profile_controller_show_get'); +$r->post('/move', function() { + /* + 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. -/* - Settings -*/ -$r->get('/settings', 'settings_controller_get'); + 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. + */ -/* - Auctions -*/ -$r->get('/auctions', 'auctions_controller_get'); + 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 { + error_response(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]); +}); /* UI */ -router_post($r, '/ui/stats', 'ui_contoller_stats_post'); - -/* - Testing -*/ -if (env('debug')) { - $r->get('/give_silver/:x', function (int $amt) { - auth_only_and_must_have_character(); - wallet()->give(Currency::Silver, $amt); - redirect('/'); - }); - - $r->get('/take_silver/:x', function (int $amt) { - auth_only_and_must_have_character(); - wallet()->take(Currency::Silver, $amt); - redirect('/'); - }); -} +$r->post('/ui/stats', function() { + ui_guard(); + echo c_profile_stats(char()); +}); /* Router diff --git a/src/bootstrap.php b/src/bootstrap.php index 93a8816..70bd483 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -10,8 +10,10 @@ define('CLASS_MAP', [ 'Wallet' => '/models/wallet.php' ]); -// Source libraries require_once SRC . '/helpers.php'; + +stopwatch_start('bootstrap'); // Start the bootstrap stopwatch + require_once SRC . '/util/env.php'; require_once SRC . '/util/database.php'; require_once SRC . '/util/auth.php'; @@ -20,19 +22,9 @@ require_once SRC . '/util/components.php'; require_once SRC . '/util/render.php'; require_once SRC . '/util/enums.php'; -// Database models require_once SRC . '/models/session.php'; require_once SRC . '/models/token.php'; -// Controllers -require_once SRC . '/controller/char.php'; -require_once SRC . '/controller/auth.php'; -require_once SRC . '/controller/world.php'; -require_once SRC . '/controller/settings.php'; -require_once SRC . '/controller/auctions.php'; -require_once SRC . '/controller/profile.php'; -require_once SRC . '/controller/ui.php'; - spl_autoload_register(function (string $class) { if (array_key_exists($class, CLASS_MAP)) require_once SRC . CLASS_MAP[$class]; }); @@ -51,8 +43,6 @@ if (env('debug') === 'true') { error_reporting(E_ALL); } -stopwatch_start('bootstrap'); // Start the bootstrap stopwatch - // Generate a new CSRF token. (if one doesn't exist, that is) csrf(); @@ -60,9 +50,6 @@ csrf(); $GLOBALS['queries'] = 0; $GLOBALS['query_time'] = 0; -// Set the default page layout -page_layout('basic'); - // Run auth_check to see if we're logged in, since it populates the user data in SESSION auth_check(); diff --git a/src/controllers/auctions.php b/src/controllers/auctions.php deleted file mode 100644 index 5194519..0000000 --- a/src/controllers/auctions.php +++ /dev/null @@ -1,9 +0,0 @@ - 'pages/auctions/index']); -} diff --git a/src/controllers/auth.php b/src/controllers/auth.php deleted file mode 100644 index 4e445ce..0000000 --- a/src/controllers/auth.php +++ /dev/null @@ -1,155 +0,0 @@ - 'pages/auth/register']); -} - -/** - * Handles the registration form submission. - */ -function auth_controller_register_post() -{ - guest_only(); - csrf_ensure(); - - $errors = []; - - $u = trim($_POST['u'] ?? ''); - $e = trim($_POST['e'] ?? ''); - $p = $_POST['p'] ?? ''; - - /* - A username is required. - A username must be at least 3 characters long and at most 18 characters long. - A username must contain only alphanumeric characters and spaces. - */ - if (empty($u) || strlen($u) < 3 || strlen($u) > 18 || !ctype_alnum(str_replace(' ', '', $u))) { - $errors['u'][] = 'Username is required and must be between 3 and 18 characters long and contain only - alphanumeric characters and spaces.'; - } - - /* - An email is required. - An email must be at most 255 characters long. - An email must be a valid email address. - */ - if (empty($e) || strlen($e) > 255 || !filter_var($e, FILTER_VALIDATE_EMAIL)) { - $errors['e'][] = 'Email is required must be a valid email address.'; - } - - /* - A password is required. - A password must be at least 6 characters long. - */ - if (empty($p) || strlen($p) < 6) { - $errors['p'][] = 'Password is required and must be at least 6 characters long.'; - } - - /* - A username must be unique. - */ - if (User::username_exists($u)) { - $errors['u'][] = 'Username is already taken.'; - } - - /* - An email must be unique. - */ - if (User::email_exists($e)) { - $errors['e'][] = 'Email is already taken.'; - } - - // If there are errors at this point, send them to the page with errors flashed. - if (!empty($errors)) { - $GLOBALS['form-errors'] = $errors; - echo page('auth/register'); - exit; - } - - if (User::create($u, $e, $p) === false) error_response(400); - - $_SESSION['user'] = serialize(User::find($u)); - Wallet::create(user()->id); - redirect('/character/create-first'); -} - -/** - * Displays the login page. - */ -function auth_controller_login_get() -{ - guest_only(); - echo render('layouts/basic', ['view' => 'pages/auth/login']); -} - -/** - * Handles the login form submission. - */ -function auth_controller_login_post() -{ - guest_only(); - csrf_ensure(); - - $errors = []; - - $u = trim($_POST['u'] ?? ''); - $p = $_POST['p'] ?? ''; - - if (empty($u)) $errors['u'][] = 'Username is required.'; - if (empty($p)) $errors['p'][] = 'Password is required.'; - - // If there are errors at this point, send them to the page with errors flashed. - if (!empty($errors)) { - $GLOBALS['form-errors'] = $errors; - echo render('layouts/basic', ['view' => 'pages/auth/login']); - exit; - } - - $user = User::find($u); - if ($user === false || !$user->check_password($p)) { - $errors['x'][] = 'Invalid username or password.'; - $GLOBALS['form-errors'] = $errors; - echo render('layouts/basic', ['view' => 'pages/auth/login']); - exit; - } - - $_SESSION['user'] = serialize($user); - - if ($_POST['remember'] ?? false) { - $token = token(); - $expires = strtotime('+30 days'); - $result = db_query( - db_auth(), - "INSERT INTO sessions (token, user_id, expires) VALUES (:t, :u, :e)", - [':t' => $token, ':u' => user()->id, ':e' => $expires] - ); - if (!$result) error_response(400); - set_cookie('remember_me', $token, $expires); - } - - if (user()->char_count() === 0) { - redirect('/character/create-first'); - } elseif (!change_user_character(user()->char_id)) { - echo "failed to change user character (aclp)"; - error_response(999); - } - - redirect('/'); -} - -/** - * Logs the user out. - */ -function auth_controller_logout_post() -{ - csrf_ensure(); - session_delete(user()->id); - unset($_SESSION['user']); - set_cookie('remember_me', '', 1); - redirect('/'); -} diff --git a/src/controllers/char.php b/src/controllers/char.php deleted file mode 100644 index 1c77a3a..0000000 --- a/src/controllers/char.php +++ /dev/null @@ -1,172 +0,0 @@ - user()->char_list()]); -} - -/** - * Handle an action from the character list page. - */ -function char_controller_list_post() -{ - auth_only_and_must_have_character(); csrf_ensure(); - - $GLOBALS['active_nav_tab'] = 'chars'; - - $char_id = (int) ($_POST['char_id'] ?? 0); - $action = $_POST['action'] ?? ''; - - // If the character ID is not a number, or the action is not a string, return a 400. - if (!is_numeric($char_id) || !is_string($action)) error_response(400); - - // If the character ID is 0, return to the list. - if ($char_id === 0) { - flash('alert_character_list_1', ['', 'No character selected.']); - redirect('/characters'); - } - - // If the action is not one of the allowed actions, return a 400. - if (!in_array($action, ['select', 'delete'])) error_response(400); - - // If the action is to select a character, change the user's selected character. - if ($action === 'select') { - // If the character ID is the current character, do nothing. - if ($char_id === user()->char_id || $char_id === 0) { - flash('alert_character_list_1', ['info', 'You are already using ' . char()->name . '.']); - redirect('/characters'); - } - - if (!Character::belongs_to($char_id, user()->id)) error_response(999); - - change_user_character($char_id); - - flash('alert_character_list_1', ['success', 'Switched to ' . char()->name . '!']); - } - - // If the action is to delete a character, move to the confirmation page. - if ($action === 'delete') { - if (!Character::belongs_to($char_id, user()->id)) error_response(999); - - echo page('chars/delete', ['char' => Character::find($char_id)]); - exit; - } - - redirect('/characters'); -} - -/** - * Delete a character for the currently logged in user. - */ -function char_controller_delete_post() -{ - auth_only_and_must_have_character(); csrf_ensure(); - - $char_id = (int) ($_POST['char_id'] ?? 0); - - // If the character ID is not a number, return a 400. - if (!is_numeric($char_id)) error_response(400); - - // Ensure the character ID is valid and belongs to the user. - if (!Character::belongs_to($char_id, user()->id)) error_response(999); - - $char = Character::find($char_id); - - // Confirm the name matches the name of the character. CASE SENSITIVE. - if ($char['name'] !== trim($_POST['n'] ?? '')) { - flash('alert_character_list_1', ['danger', 'Failed to delete ' . $char['name'] . '. Name confirmation did not match.']); - redirect('/characters'); - } - - // Delete the character - Character::delete($char_id); - - // If the character being deleted is the currently selected character, select the first character. - if (user()->char_id === $char_id) { - $chars = user()->char_list(); - if (count($chars) > 0) change_user_character($chars[0]['id']); - } - - flash('alert_character_list_1', ['danger', 'Character ' . $char['name'] . ' deleted.']); - redirect('/characters'); -} - -/** - * Form to create your first character. - */ -function char_controller_create_first_get() -{ - auth_only(); - - $GLOBALS['active_nav_tab'] = 'chars'; - - // If the user already has a character, redirect them to the main page. - if (user()->char_count() > 0) redirect('/'); - - echo page('chars/first'); -} - -/** - * Create a character for the currently logged in user. - */ -function char_controller_create_post() -{ - auth_only(); csrf_ensure(); - - $GLOBALS['active_nav_tab'] = 'chars'; - - $errors = []; - - $name = trim($_POST['n'] ?? ''); - - /* - A name is required. - A name must be between 3 and 18 characters. - A name must contain only alphanumeric characters and spaces. - */ - if (empty($name) || strlen($name) < 3 || strlen($name) > 18 || !ctype_alnum(str_replace(' ', '', $name))) { - $errors['n'][] = 'Name is required and must be between 3 and 18 characters long and contain only alphanumeric characters and spaces.'; - } - - /* - A character's name must be unique. - */ - if (Character::name_exists($name)) $errors['n'][] = 'Name is already taken.'; - - // If there are errors at this point, send them to the page with errors flashed. - if (!empty($errors)) { - $GLOBALS['form-errors-create-character'] = $errors; - - if (isset($_POST['first']) && $_POST['first'] === 'true') { - // If this is the first character, return to the first character creation page. - echo page('chars/first'); - exit; - } else { - // If this is not the first character, return to the character list page. - echo page('chars/list', ['chars' => user()->char_list()]); - exit; - } - } - - if (($char = Character::create(user()->id, $name)) === false) error_response(400); - - // Create the auxiliary tables - $char->create_location(); - $char->create_gear(); - - // Award the Adventurer title. - $char->award_title(1); - - // Set the character as the user's selected character - change_user_character($char->id); - - flash('alert_character_list_1', ['success', 'Character ' . $name . ' created!']); - redirect('/characters'); -} - diff --git a/src/controllers/profile.php b/src/controllers/profile.php deleted file mode 100644 index 3497d13..0000000 --- a/src/controllers/profile.php +++ /dev/null @@ -1,24 +0,0 @@ -char_id == $id) redirect('/profile'); - echo page('profile/show', ['c' => $char]); -} diff --git a/src/controllers/settings.php b/src/controllers/settings.php deleted file mode 100644 index b2521ff..0000000 --- a/src/controllers/settings.php +++ /dev/null @@ -1,9 +0,0 @@ - 'pages/settings/index']); -} diff --git a/src/controllers/ui.php b/src/controllers/ui.php deleted file mode 100644 index 3c3174b..0000000 --- a/src/controllers/ui.php +++ /dev/null @@ -1,7 +0,0 @@ - $x, - ':y' => $y, - ':c' => user()->char_id - ]); - - if ($r === false) throw new Exception('Failed to move character. (wcmp)'); - - json_response(['x' => $x, 'y' => $y]); -} diff --git a/src/helpers.php b/src/helpers.php index 44119c1..1be12eb 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -3,7 +3,7 @@ /** * Generate a pretty dope token. */ -function token($length = 32) +function token($length = 32): string { return bin2hex(random_bytes($length)); } @@ -11,7 +11,7 @@ function token($length = 32) /** * Redirect to a new location. */ -function redirect($location) +function redirect($location): void { header("Location: $location"); exit; @@ -353,3 +353,14 @@ function parse_bbcode(string $text): array 'char_count' => $charCount ]; } + +/** + * Shorthand to verify auth, a character is selected, CSRF is correct, and it is an AJAX request. Used for + * front-end API routes. + */ +function ui_guard() +{ + auth_only_and_must_have_character(); + ajax_only(); + csrf_ensure(); +} diff --git a/src/util/components.php b/src/util/components.php index 9b2cccc..8a97716 100644 --- a/src/util/components.php +++ b/src/util/components.php @@ -117,3 +117,12 @@ function c_equipped_gear(Character $char): string { return render('components/equipped_gear', ['char' => $char]); } + +/** + * Render a front-end UI window with the given title and content. The WindowManager on the front end will handle + * the rest. + */ +function c_ui_window(string $title = '', string $content = ''): string +{ + return render('components/ui_window', ['title' => $title, 'content' => $content]); +} diff --git a/src/util/render.php b/src/util/render.php index 8954697..9072796 100644 --- a/src/util/render.php +++ b/src/util/render.php @@ -18,20 +18,3 @@ function render($pathToBaseView, $data = []) require template($pathToBaseView); return ob_get_clean(); } - -/** - * Set/retrieve the current page layout in/from GLOBALS. - */ -function page_layout($layout = '') -{ - if ($layout !== '') $GLOBALS['page-layout'] = $layout; - return $GLOBALS['page-layout'] ?? 'basic'; -} - -/** - * Shorthand to render a page with the current layout. - */ -function page($view, $data = []) -{ - return render("layouts/" . page_layout(), ['view' => "pages/$view"] + $data); -} diff --git a/src/util/router.php b/src/util/router.php index 34e24cb..d77bea7 100644 --- a/src/util/router.php +++ b/src/util/router.php @@ -1,5 +1,9 @@ 405, 'handler' => null, 'params' => []]; } + /** + * Shorthand to register a GET route. + */ public function get(string $route, callable $handler): Router { return $this->add('GET', $route, $handler); } + /** + * Shorthand to register a POST route. + */ public function post(string $route, callable $handler): Router { return $this->add('POST', $route, $handler); diff --git a/templates/components/debug_query_log.php b/templates/components/debug_query_log.php deleted file mode 100644 index a533452..0000000 --- a/templates/components/debug_query_log.php +++ /dev/null @@ -1,11 +0,0 @@ -
= $GLOBALS['queries'] ?> queries were executed.
- ({$time}s) {$query[0]}"; - } - ?> -Page execution took = number_format((microtime(true) - START_TIME), 10) ?> seconds.
-Bootstrap: = stopwatch_get('bootstrap') ?> seconds
-Router: = stopwatch_get('router') ?> seconds
-Handler: = stopwatch_get('handler') ?> seconds
-