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
-