From d3eeaf6cedd68c840ce6232f3c7abe076403de4a Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Wed, 23 Oct 2024 17:39:48 -0500 Subject: [PATCH] Class refactor, move char bar --- public/assets/css/dragon.css | 2 +- public/assets/css/src/main.css | 34 +-- public/index.php | 17 ++ src/bootstrap.php | 17 +- src/controller/auth.php | 35 ++- src/controller/char.php | 44 ++-- src/controller/profile.php | 2 +- src/controller/world.php | 2 +- src/helpers.php | 82 ++++--- src/model/char.php | 284 ----------------------- src/model/user.php | 52 ----- src/models/character.php | 262 +++++++++++++++++++++ src/{model => models}/fights.php | 0 src/{model => models}/items.php | 0 src/{model => models}/session.php | 0 src/{model => models}/token.php | 0 src/models/user.php | 180 ++++++++++++++ src/models/wallet.php | 52 +++++ src/util/auth.php | 34 +-- src/util/components.php | 8 + src/util/database.php | 11 +- src/util/enums.php | 25 ++ templates/components/char_bar.php | 36 --- templates/components/char_select_box.php | 4 +- templates/components/profile_stats.php | 24 +- templates/components/right_nav.php | 37 +++ templates/layouts/basic.php | 12 +- templates/pages/chars/list.php | 6 +- templates/pages/home.php | 2 +- templates/pages/profile/main.php | 6 +- templates/pages/profile/show.php | 2 +- 31 files changed, 726 insertions(+), 546 deletions(-) delete mode 100644 src/model/char.php delete mode 100644 src/model/user.php create mode 100644 src/models/character.php rename src/{model => models}/fights.php (100%) rename src/{model => models}/items.php (100%) rename src/{model => models}/session.php (100%) rename src/{model => models}/token.php (100%) create mode 100644 src/models/user.php create mode 100644 src/models/wallet.php create mode 100644 src/util/enums.php delete mode 100644 templates/components/char_bar.php diff --git a/public/assets/css/dragon.css b/public/assets/css/dragon.css index e5af3f4..b343c47 100644 --- a/public/assets/css/dragon.css +++ b/public/assets/css/dragon.css @@ -1 +1 @@ -: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:#0000004d;margin-bottom:.5rem}& div.stats{&>.grid{grid-template-columns:1fr 1fr;gap:.25rem;display:grid;&>div.cell{color:#fff;background:#0000004d;border-radius:.15rem;justify-content:space-between;align-items:center;padding:.25rem .5rem;display:flex;& .label{text-transform:uppercase;color:#ffffffbf;margin-right:.25rem;font-size:.75rem}}}}}body{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;max-width:1640px;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%;min-width:968px;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}}#char-bar{color:#fff;background-image:url(/assets/img/bar.jpg);justify-content:space-around;align-items:center;gap:1rem;height:34px;padding:0 1rem;display:flex;&>div.container{align-items:center;gap:1rem;display:flex;&>div{align-items:center;display:flex;& .icon{width:18px;margin-right:.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:2rem;font-family:monospace;font-size:14px}#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{image-rendering:pixelated;image-rendering:crisp-edges;image-rendering:-webkit-optimize-contrast;width:100%;height:440px;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}} \ No newline at end of file +: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:#0000004d;margin-bottom:.5rem}& div.stats{&>.grid{grid-template-columns:1fr 1fr;gap:.25rem;display:grid;&>div.cell{color:#fff;background:#0000004d;border-radius:.15rem;justify-content:space-between;align-items:center;padding:.25rem .5rem;display:flex;& .label{text-transform:uppercase;color:#ffffffbf;margin-right:.25rem;font-size:.75rem}}}}}body{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;min-width:968px;max-width:1640px;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{image-rendering:pixelated;image-rendering:crisp-edges;image-rendering:-webkit-optimize-contrast;width:100%;height:440px;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}} \ No newline at end of file diff --git a/public/assets/css/src/main.css b/public/assets/css/src/main.css index cecb1c8..eba40c1 100644 --- a/public/assets/css/src/main.css +++ b/public/assets/css/src/main.css @@ -10,6 +10,7 @@ body { background-position: center top; background-repeat: no-repeat; max-width: 1640px; + min-width: 968px; margin: 0px auto; font-family: var(--main-font); } @@ -41,7 +42,6 @@ header#main-header { main { padding: 1rem; width: 100%; - min-width: 968px; display: flex; gap: 2rem; @@ -208,30 +208,14 @@ footer { } } -#char-bar { - display: flex; - align-items: center; - justify-content: space-around; - padding: 0 1rem; - height: 34px; - color: white; - gap: 1rem; - background-image: url('/assets/img/bar.jpg'); - - & > div.container { +#character { + & > .name { display: flex; align-items: center; - gap: 1rem; + } - & > div { - display: flex; - align-items: center; - - .icon { - width: 18px; - margin-right: 0.5rem; - } - } + & > div:not(:last-child) { + margin-bottom: 0.5rem; } } @@ -311,10 +295,14 @@ span.badge { } .debug-query-log { - padding: 2rem; + padding: 1rem; font-size: 14px; color: #666; font-family: monospace; + + &:last-child { + padding-top: 0; + } } #center > section { diff --git a/public/index.php b/public/index.php index 24b9007..469e3d3 100644 --- a/public/index.php +++ b/public/index.php @@ -57,6 +57,23 @@ router_get($r, '/settings', 'settings_controller_get'); */ router_get($r, '/auctions', 'auctions_controller_get'); +/* + Testing +*/ +if (env('debug')) { + router_get($r, '/give_silver/:x', function (int $amt) { + auth_only_and_must_have_character(); + wallet()->give(Currency::Silver, $amt); + redirect('/'); + }); + + router_get($r, '/take_silver/:x', function (int $amt) { + auth_only_and_must_have_character(); + wallet()->take(Currency::Silver, $amt); + redirect('/'); + }); +} + /* Router */ diff --git a/src/bootstrap.php b/src/bootstrap.php index a829c0a..e751483 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -4,6 +4,12 @@ session_start(); // SRC is defined as the path to the src/ directory from public/ +define('CLASS_MAP', [ + 'User' => '/models/user.php', + 'Character' => '/models/character.php', + 'Wallet' => '/models/wallet.php' +]); + // Source libraries require_once SRC . '/helpers.php'; require_once SRC . '/util/env.php'; @@ -12,12 +18,11 @@ require_once SRC . '/util/auth.php'; require_once SRC . '/util/router.php'; require_once SRC . '/util/components.php'; require_once SRC . '/util/render.php'; +require_once SRC . '/util/enums.php'; // Database models -require_once SRC . '/model/user.php'; -require_once SRC . '/model/session.php'; -require_once SRC . '/model/token.php'; -require_once SRC . '/model/char.php'; +require_once SRC . '/models/session.php'; +require_once SRC . '/models/token.php'; // Controllers require_once SRC . '/controller/char.php'; @@ -27,6 +32,10 @@ require_once SRC . '/controller/settings.php'; require_once SRC . '/controller/auctions.php'; require_once SRC . '/controller/profile.php'; +spl_autoload_register(function (string $class) { + if (array_key_exists($class, CLASS_MAP)) require_once SRC . CLASS_MAP[$class]; +}); + // Track the start time of the request define('START_TIME', microtime(true)); diff --git a/src/controller/auth.php b/src/controller/auth.php index b7cd9f6..5f6336f 100644 --- a/src/controller/auth.php +++ b/src/controller/auth.php @@ -53,14 +53,14 @@ function auth_controller_register_post() /* A username must be unique. */ - if (auth_username_exists($u)) { + if (User::username_exists($u)) { $errors['u'][] = 'Username is already taken.'; } /* An email must be unique. */ - if (auth_email_exists($e)) { + if (User::email_exists($e)) { $errors['e'][] = 'Email is already taken.'; } @@ -71,11 +71,10 @@ function auth_controller_register_post() exit; } - $user = user_create($u, $e, $p); - if ($user === false) router_error(400); + if (User::create($u, $e, $p) === false) router_error(400); - $_SESSION['user'] = user_find($u); - wallet_create($_SESSION['user']['id']); + $_SESSION['user'] = serialize(User::find($u)); + Wallet::create(user()->id); redirect('/character/create-first'); } @@ -111,31 +110,32 @@ function auth_controller_login_post() exit; } - $user = user_find($u); - if ($user === false || !password_verify($p, $user['password'])) { + $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'] = $user; + $_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' => $_SESSION['user']['id'], - ':e' => $expires - ]); + $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) router_error(400); set_cookie('remember_me', $token, $expires); } - if (char_count($_SESSION['user']['id']) === 0) { + if (user()->char_count() === 0) { redirect('/character/create-first'); - } elseif (!change_user_character($_SESSION['user']['char_id'])) { + } elseif (!change_user_character(user()->char_id)) { + echo "failed to change user character (aclp)"; router_error(999); } @@ -148,9 +148,8 @@ function auth_controller_login_post() function auth_controller_logout_post() { csrf_ensure(); - session_delete($_SESSION['user']['id']); + session_delete(user()->id); unset($_SESSION['user']); - unset($_SESSION['char']); set_cookie('remember_me', '', 1); redirect('/'); } diff --git a/src/controller/char.php b/src/controller/char.php index f34bb6b..9f0df5e 100644 --- a/src/controller/char.php +++ b/src/controller/char.php @@ -8,7 +8,7 @@ function char_controller_list_get() auth_only_and_must_have_character(); $GLOBALS['active_nav_tab'] = 'chars'; - echo page('chars/list', ['chars' => char_list(user('id'))]); + echo page('chars/list', ['chars' => user()->char_list()]); } /** @@ -38,25 +38,23 @@ function char_controller_list_post() // 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 === $_SESSION['user']['char_id'] || $char_id === 0) { - flash('alert_character_list_1', ['info', 'You are already using ' . char('name') . '.']); + if ($char_id === user()->char_id || $char_id === 0) { + flash('alert_character_list_1', ['info', 'You are already using ' . char()->name . '.']); redirect('/characters'); } - // Ensure the character ID is valid and belongs to the user. - if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999); + if (!Character::belongs_to($char_id, user()->id)) router_error(999); change_user_character($char_id); - flash('alert_character_list_1', ['success', 'Switched to ' . char('name') . '!']); + 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') { - // Ensure the character ID is valid and belongs to the user. - if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999); + if (!Character::belongs_to($char_id, user()->id)) router_error(999); - echo page('chars/delete', ['char' => char_find($char_id)]); + echo page('chars/delete', ['char' => Character::find($char_id)]); exit; } @@ -76,9 +74,9 @@ function char_controller_delete_post() if (!is_numeric($char_id)) router_error(400); // Ensure the character ID is valid and belongs to the user. - if (!char_belongs_to_user($char_id, $_SESSION['user']['id'])) router_error(999); + if (!Character::belongs_to($char_id, user()->id)) router_error(999); - $char = char_find($char_id); + $char = Character::find($char_id); // Confirm the name matches the name of the character. CASE SENSITIVE. if ($char['name'] !== trim($_POST['n'] ?? '')) { @@ -87,11 +85,11 @@ function char_controller_delete_post() } // Delete the character - char_delete($char_id); + Character::delete($char_id); // If the character being deleted is the currently selected character, select the first character. - if ($_SESSION['user']['char_id'] === $char_id) { - $chars = char_list(user('id')); + if (user()->char_id === $char_id) { + $chars = user()->char_list(); if (count($chars) > 0) change_user_character($chars[0]['id']); } @@ -109,7 +107,7 @@ function char_controller_create_first_get() $GLOBALS['active_nav_tab'] = 'chars'; // If the user already has a character, redirect them to the main page. - if (char_count(user('id')) > 0) redirect('/'); + if (user()->char_count() > 0) redirect('/'); echo page('chars/first'); } @@ -139,7 +137,7 @@ function char_controller_create_post() /* A character's name must be unique. */ - if (char_name_exists($name)) $errors['n'][] = 'Name is already taken.'; + 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)) { @@ -151,24 +149,22 @@ function char_controller_create_post() exit; } else { // If this is not the first character, return to the character list page. - echo page('chars/list', ['chars' => char_list(user('id'))]); + echo page('chars/list', ['chars' => user()->char_list()]); exit; } } - // Create the character - $char = char_create(user('id'), $name); - if ($char === false) router_error(400); + if (($char = Character::create(user()->id, $name)) === false) router_error(400); // Create the auxiliary tables - char_location_create($char); - char_gear_create($char); + $char->create_location(); + $char->create_gear(); // Award the Adventurer title. - char_award_title(1, $char); + $char->award_title(1); // Set the character as the user's selected character - change_user_character($char); + change_user_character($char->id); flash('alert_character_list_1', ['success', 'Character ' . $name . ' created!']); redirect('/characters'); diff --git a/src/controller/profile.php b/src/controller/profile.php index ea5d48e..ddec6e0 100644 --- a/src/controller/profile.php +++ b/src/controller/profile.php @@ -19,6 +19,6 @@ function profile_controller_show_get($id) auth_only_and_must_have_character(); if (($char = char_find($id)) == false) router_error(999); - if (user('char_id') == $id) redirect('/profile'); + if (user()->char_id == $id) redirect('/profile'); echo page('profile/show', ['char' => $char]); } diff --git a/src/controller/world.php b/src/controller/world.php index de5d989..7567520 100644 --- a/src/controller/world.php +++ b/src/controller/world.php @@ -51,7 +51,7 @@ function world_controller_move_post() $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') + ':c' => user()->char_id ]); if ($r === false) throw new Exception('Failed to move character. (wcmp)'); diff --git a/src/helpers.php b/src/helpers.php index c654434..34a4534 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -94,58 +94,51 @@ function set_cookie($name, $value, $expires) } /** - * Get the current user's array from SESSION if it exists. Specify a key to get a specific value. + * Get the current user's object from SESSION if it exists. */ -function user($field = '') +function user(): User|false { if (empty($_SESSION['user'])) return false; - if ($field === '') return $_SESSION['user']; - return $_SESSION['user'][$field] ?? false; + return unserialize($_SESSION['user']); } /** - * Check whether the user has selected a character. + * Modify a field in the user session object. Returns true on success and false on failure. */ -function user_selected_char() +function modify_user_session(string $field, mixed $value): bool { - return user('char_id') > 0 ? true : false; + $user = user(); + if ($user === false || !property_exists($user, $field)) return false; + $user->{$field} = $value; + $_SESSION['user'] = serialize($user); + return true; } /** - * If the current user has a selected char and the data is in the session, retrieve either the full array of data - * or a specific field. If there is no character data, populate it. + * If the current user has a selected char and the data is in the session, retrieve the character object. If there + * is no character data, populate it. */ -function char($field = '') +function char(): Character|false { - // If there is no user, return false if (empty($_SESSION['user'])) return false; - - if (empty($GLOBALS['char'])) { - $GLOBALS['char'] = db_query( - db_live(), - "SELECT * FROM characters WHERE id = :c", - [':c' => user('char_id')] - )->fetchArray(SQLITE3_ASSOC); - } - - if ($field === '') return $GLOBALS['char']; - return $GLOBALS['char'][$field] ?? false; + if (empty($GLOBALS['char'])) $GLOBALS['char'] = serialize(user()->current_char()); + return unserialize($GLOBALS['char']); } /** - * Shorthand to update the user's selected character. Returns true on success, false on failure. Database + * Update the user's selected character. Returns true on success, false on failure. Database * is updated if the character ID is different from the current session. */ -function change_user_character($char_id) +function change_user_character(int $char_id): bool { // If the character does not exist, return false - if (($char = char_find($char_id)) === false) return false; - $GLOBALS['char'] = $char; + if (($char = Character::find($char_id)) === false) return false; + $GLOBALS['char'] = serialize($char); // If the character ID is different, update the session and database - if ($_SESSION['user']['char_id'] !== $char_id) { - $_SESSION['user']['char_id'] = $char_id; - db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user('id')]); + if (user()->char_id !== $char_id) { + modify_user_session('char_id', $char_id); + db_query(db_auth(), "UPDATE users SET char_id = :c WHERE id = :u", [':c' => $char_id, ':u' => user()->id]); } return true; @@ -154,7 +147,7 @@ function change_user_character($char_id) /** * Get a percent between two ints, rounded to the nearest whole number or return 0. */ -function percent($num, $denom, $precision = 4): int +function percent(float $num, float $denom, int $precision = 4): float { if ($denom === 0) return 0; $p = ($num / $denom) * 100; @@ -164,20 +157,14 @@ function percent($num, $denom, $precision = 4): int /** * Access the account wallet. On first execution it will populate $GLOBALS['wallet'] with the wallet data. This way * the data is up to date with every request without having to query the database every use within, for example, a - * template. Will return false if the field does not exist, or the entire wallet array if no field is specified. + * template. Will return false if the user or wallet does not exist. */ -function wallet($field = '') +function wallet(): Wallet|false { - if (empty($GLOBALS['wallet'])) { - $GLOBALS['wallet'] = db_query( - db_live(), - "SELECT * FROM wallets WHERE user_id = :u", - [':u' => user('id')] - )->fetchArray(SQLITE3_ASSOC); - } - - if ($field === '') return $GLOBALS['wallet']; - return $GLOBALS['wallet'][$field] ?? false; + if (user() === false) return false; + if (empty($GLOBALS['wallet'])) $w = user()->wallet(); + if ($w === false) return false; + return $GLOBALS['wallet'] = $w; } /** @@ -191,7 +178,7 @@ function location($field = '') $GLOBALS['location'] = db_query( db_live(), "SELECT * FROM char_locations WHERE char_id = :c", - [':c' => user('char_id')] + [':c' => user()->char_id] )->fetchArray(SQLITE3_ASSOC); } @@ -300,3 +287,12 @@ function abb_num($num) default => number_format($num, 0) }; } + +/** + * Check if all keys of an array are numeric. + */ +function all_keys_numeric(array $a): bool +{ + foreach (array_keys($a) as $k) if (!is_int($k)) return false; + return true; +} diff --git a/src/model/char.php b/src/model/char.php deleted file mode 100644 index 75242b9..0000000 --- a/src/model/char.php +++ /dev/null @@ -1,284 +0,0 @@ - 'Exploring', - 1 => 'In Town', - 2 => 'In Combat', - 4 => 'In Shop', - 5 => 'In Inn' -]; - -/** - * Create a character. Only a user ID and a name are required. All other fields are optional. Pass a key-value array - * of overrides to set additional fields. A character's name must be unique, but this function does not check for - * that. Returns the created character's ID. - */ -function char_create($user_id, $name, $overrides = []): int -{ - // Prep the data and merge in any overrides - $data = ['user_id' => $user_id, 'name' => $name]; - if (!empty($overrides)) $data = array_merge($data, $overrides); - - // Prep the fields for the query - $k = array_keys($data); - $f = implode(', ', $k); - $v = implode(', ', array_map(fn($x) => ":$x", $k)); - - // Create the character! - if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) { - // @TODO: Log this error - throw new Exception('Failed to create character. (cc)'); - } - - // Get the character ID - return db_live()->lastInsertRowID(); -} - -/** - * Create a character's location record. A character's location is where they are in the game world. A character can only be - * in one location at a time. Can define a starting location for the character. Default state is 'Exploring'. - */ -function char_location_create($char_id, $x = 0, $y = 0, $currently = 0) -{ - if (db_query(db_live(), "INSERT INTO char_locations (char_id, x, y, currently) VALUES (:p, :x, :y, :c)", [ - ':p' => $char_id, - ':x' => $x, - ':y' => $y, - ':c' => $currently - ]) === false) { - throw new Exception('Failed to create character location. (clc)'); - } -} - -/** - * Create the character's gear table. A character's gear is where they store their equipped items. - * @TODO: implement initial gear - */ -function char_gear_create($char_id, $initialGear = []) -{ - if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) { - throw new Exception('Failed to create character gear. (cgc)'); - } -} - -/** - * Get a charcter by their ID or name. Returns the character's data as an associative array, or false if not found. - */ -function char_find($char_id) -{ - $char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id OR name = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC); - return $char === false ? false : $char; -} - -/** - * Count the number of characters associated with an account ID. - */ -function char_count($user_id): int -{ - $count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM); - if ($count === false) throw new Exception('Failed to count characters. (cc)'); - return (int) $count[0]; -} - -/** - * Get a an array of id => [name, level] for all characters associated with an account ID. - */ -function char_list($user_id) -{ - $stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $user_id]); - if ($stmt === false) throw new Exception('Failed to list characters. (cl)'); - - $characters = []; - while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) { - $characters[$row['id']] = ['name' => $row['name'], 'level' => $row['level']]; - } - - return $characters; -} - -/** - * Get a character's location info by their character ID. Returns the location's data as an associative array. - */ -function char_get_location($char_id) -{ - // Get the location - $location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); - if ($location === false) throw new Exception('Location not found. (cgl)'); - return $location; -} - -/** - * See if a character name exists. - */ -function char_name_exists($name) -{ - return db_exists(db_live(), 'characters', 'name', $name); -} - -/** - * Checks whether a character exists at a certain ID. - */ -function char_exists($char_id) -{ - return db_exists(db_live(), 'characters', 'id', $char_id); -} - -/** - * See if the given character belongs to the given user. Returns false if the character does not belong to the user, - * or if the character does not exist. Returns true if the character belongs to the user. Generally this function - * shouldn't return false, as it should be called after the character's existence is confirmed. If it does return false, - * it is likely due to user interference. - */ -function char_belongs_to_user($char_id, $user_id) -{ - $char = db_query( - db_live(), - "SELECT 1 FROM characters WHERE id = :p AND user_id = :u", - [':p' => $char_id, ':u' => $user_id] - )->fetchArray(SQLITE3_ASSOC); - return $char !== false; -} - -/** - * Delete a character by their ID. This will delete all associated data tables as well. - */ -function char_delete($char_id) -{ - // Delete the character - if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $char_id]) === false) { - throw new Exception('Failed to delete character. (cd)'); - } - - // Get item IDs from the character's inventory - $items = db_query(db_live(), "SELECT item_id FROM char_inventory WHERE char_id = :p", [':p' => $char_id]); - // delete the character's inventory and items - while ($row = $items->fetchArray(SQLITE3_ASSOC)) { - if (db_query(db_live(), "DELETE FROM char_inventory WHERE char_id = :c", [':c' => $char_id]) === false) { - throw new Exception('Failed to delete character inventory. (cd)'); - } - - if (db_query(db_live(), "DELETE FROM items WHERE id = :p", [':p' => $row['id']]) === false) { - throw new Exception('Failed to delete character item slots. (cd)'); - } - } - - // Delete the character's location - if (db_query(db_live(), "DELETE FROM char_locations WHERE char_id = :p", [':p' => $char_id]) === false) { - throw new Exception('Failed to delete character location. (cd)'); - } - - // Delete the character's gear - if (db_query(db_live(), "DELETE FROM char_gear WHERE char_id = :p", [':p' => $char_id]) === false) { - throw new Exception('Failed to delete character gear. (cd)'); - } - - // Delete the character's bank - if (db_query(db_live(), "DELETE FROM char_bank WHERE char_id = :p", [':p' => $char_id]) === false) { - throw new Exception('Failed to delete character bank. (cd)'); - } - - // Delete character's banked items - if (db_query(db_live(), "DELETE FROM char_banked_items WHERE char_id = :p", [':p' => $char_id]) === false) { - throw new Exception('Failed to delete character bank items. (cd)'); - } - - // Delete the user's guild membership - if (db_query(db_live(), "DELETE FROM guild_members WHERE char_id = :p", [':p' => $char_id]) === false) { - throw new Exception('Failed to delete character guild membership. (cd)'); - } - - // if the character was a guild leader, hand leadership to the next highest ranking member - $guild = db_query(db_live(), "SELECT id FROM guilds WHERE leader_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC); - if ($guild !== false) { - $members = db_query(db_live(), "SELECT char_id FROM guild_members WHERE guild_id = :p ORDER BY rank DESC", [':p' => $guild['id']]); - $newLeader = $members->fetchArray(SQLITE3_ASSOC); - if ($newLeader !== false) { - db_query(db_live(), "UPDATE guilds SET leader_id = :p WHERE id = :g", [':p' => $newLeader['char_id'], ':g' => $guild['id']]); - } - } - - // Get a list of all pve fight IDs. - $pve = db_query(db_fights(), "SELECT id FROM pve WHERE char_id = :p", [':p' => $char_id]); - // Get a list of all pvp fight IDs. - $pvp = db_query(db_fights(), "SELECT id FROM pvp WHERE char1_id = :p OR char2_id = :p", [':p' => $char_id]); - - // Delete all pve fights - while ($row = $pve->fetchArray(SQLITE3_ASSOC)) { - if (db_query(db_fights(), "DELETE FROM pve WHERE id = :p", [':p' => $row['id']]) === false) { - throw new Exception('Failed to delete pve fight. (cd)'); - } - - if (db_query(db_fights(), "DELETE FROM pve_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) { - throw new Exception('Failed to delete pve fight logs. (cd)'); - } - } - - // Delete all pvp fights - while ($row = $pvp->fetchArray(SQLITE3_ASSOC)) { - if (db_query(db_fights(), "DELETE FROM pvp WHERE id = :p", [':p' => $row['id']]) === false) { - throw new Exception('Failed to delete pvp fight. (cd)'); - } - - if (db_query(db_fights(), "DELETE FROM pvp_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) { - throw new Exception('Failed to delete pvp fight logs. (cd)'); - } - } -} - -/** - * Award a character a title. - */ -function char_award_title($title_id, $char_id) -{ - $r = db_query( - db_live(), - 'INSERT INTO owned_titles (`title_id`, `char_id`) VALUES (:t, :c)', - [':t' => $title_id, ':c' => $char_id] - ); - if ($r === false) throw new Exception("Failed to award $char_id the title $title_id. (cat)"); -} - -/** - * Get the character's title's info and award date. Will use the currently logged in character's ID if $char_id is 0. - */ -function char_get_title($char_id = 0) -{ - $char = $char_id === 0 ? char() : char_find($char_id); - $title = title($char['title_id']); - - $stmt = db_query( - db_live(), - 'SELECT awarded FROM owned_titles WHERE char_id = :c AND title_id = :t LIMIT 1', - [':c' => $char['id'], ':t' => $char['title_id']] - ); - - // If the query failed, send back an array with only an error. - if ($stmt === false) return ['error' => "owned titles query failed {$char['id']} (cat) (1)"]; - - $award = $stmt->fetchArray(SQLITE3_ASSOC); - - // If no title, send back an empty array - if (!$award) return []; - - $title['awarded'] = $award['awarded']; - return $title; -} - -/** - * Get the character's user data. - */ -function char_get_user($char) -{ - return user_find($char['user_id']); -} diff --git a/src/model/user.php b/src/model/user.php deleted file mode 100644 index 5dd28fd..0000000 --- a/src/model/user.php +++ /dev/null @@ -1,52 +0,0 @@ - $user]); - $user = $result->fetchArray(SQLITE3_ASSOC); - if (!$user) return false; - $result->finalize(); - return $user; -} - -/** - * Create a user with a username, email, and password. Optionally pass an auth level. This function will not check - * if the username or email already exists. It is up to the caller to check this before calling this function. It is - * also up to the caller to validate password strength. This function will hash the password with the PASSWORD_ARGON2ID - * algorithm. - */ -function user_create($username, $email, $password, $auth = 0) -{ - return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [ - ':u' => $username, - ':e' => $email, - ':p' => password_hash($password, PASSWORD_ARGON2ID), - ':a' => $auth - ]); -} - -/** - * Delete a user by username, email, or id. - */ -function user_delete($user) -{ - return db_query(db_auth(), "DELETE FROM users WHERE username = :u OR email = :u OR id = :u", [':u' => $user]); -} - -/** - * Creates an account wallet. Can optionally specify the starting balances of the wallet. Returns the created wallet's - * ID. If a currency is set to -1, the starting_silver or starting_star_gems fields from the env will be used. - */ -function wallet_create($user_id, $silver = -1, $starGems = -1) -{ - if (db_query(db_live(), "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", [ - ':u' => $user_id, - ':s' => $silver === -1 ? env('start_silver', 10) : $silver, - ':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems - ]) === false) { - throw new Exception('Failed to create wallet. (wc)'); - } -} diff --git a/src/models/character.php b/src/models/character.php new file mode 100644 index 0000000..dd96978 --- /dev/null +++ b/src/models/character.php @@ -0,0 +1,262 @@ + $v) { + if (property_exists($this, $k)) $this->$k = $v; + } + } + + public static function find(int $id): Character|false + { + $q = db_query( + db_live(), + "SELECT * FROM characters WHERE id = :id OR name = :id COLLATE NOCASE", + [':id' => $id] + ); + if ($q === false) throw new Exception('Failed to query character. (C::f)'); // badly formed query + return ($c = $q->fetchArray(SQLITE3_ASSOC)) === false ? false : new Character($c); + } + + public static function create(int $user_id, string $name, array $overrides = []): Character|false + { + // Prep the data and merge in any overrides + $data = ['user_id' => $user_id, 'name' => $name]; + if (!empty($overrides)) $data = array_merge($data, $overrides); + + // Prep the fields for the query + $k = array_keys($data); + $f = implode(', ', $k); + $v = implode(', ', array_map(fn($x) => ":$x", $k)); + + // Create the character! + if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) { + // @TODO: Log this error + throw new Exception('Failed to create character. (cc)'); + } + + // Get the character ID + return Character::find(db_live()->lastInsertRowID()); + } + + /** + * Create an associated location row for this character. + */ + public function create_location(int $x = 0, int $y = 0, int $currently = 0): bool + { + $l = db_query( + db_live(), + "INSERT INTO char_locations (char_id, x, y, currently) VALUES (:i, :x, :y, :c)", + [':i' => $this->id, ':x' => $x, ':y' => $y, ':c' => $currently] + ); + return $l !== false; + } + + /** + * Create an associated gear row for this character. + */ + public function create_gear(array $initialGear = []): bool + { + // @TODO implement initial gear + $g = db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:i)", [':i' => $this->id]); + return $g !== false; + } + + /** + * Return whether a given character name exists already. + */ + public static function name_exists(string $name): bool + { + return db_exists(db_live(), 'characters', 'name', $name); + } + + /** + * Return whether a character exists with the given ID. + */ + public static function exists(int $id): bool + { + return db_exists(db_live(), 'characters', 'id', $id); + } + + /** + * Return whether a character belongs to the given user ID. + */ + public static function belongs_to(int $id, int $user_id): bool + { + $q = db_query( + db_live(), + "SELECT 1 FROM characters WHERE id = :i AND user_id = :u LIMIT 1", + [':i' => $id, ':u' => $user_id] + ); + if ($q === false) throw new Exception('Failed to query char ownership. (C::bt)'); + return $q->fetchArray(SQLITE3_ASSOC) !== false; + } + + /** + * Get the character's user. + */ + public function user(): User|false + { + return User::find($this->user_id); + } + + /** + * Award this character a title. + */ + public function award_title(int $title_id): bool + { + $r = db_query( + db_live(), + 'INSERT INTO owned_titles (`title_id`, `char_id`) VALUES (:t, :i)', + [':t' => $title_id, ':i' => $this->id] + ); + return $r !== false; + } + + /** + * Delete a character by the ID. Deletes many rows of data from many tables; items, bank, PvE and PvP w/ logs, etc + */ + public static function delete(int $id) + { + // Delete the character + if (db_query(db_live(), "DELETE FROM characters WHERE id = :p", [':p' => $id]) === false) { + throw new Exception('Failed to delete character. (C::d)'); + } + + // Get item IDs from the character's inventory + $items = db_query(db_live(), "SELECT item_id FROM char_inventory WHERE char_id = :p", [':p' => $id]); + // delete the character's inventory and items + while ($row = $items->fetchArray(SQLITE3_ASSOC)) { + if (db_query(db_live(), "DELETE FROM char_inventory WHERE char_id = :c", [':c' => $id]) === false) { + throw new Exception('Failed to delete character inventory. (C::d)'); + } + + if (db_query(db_live(), "DELETE FROM items WHERE id = :p", [':p' => $row['id']]) === false) { + throw new Exception('Failed to delete character item slots. (C::d)'); + } + } + + // Delete the character's location + if (db_query(db_live(), "DELETE FROM char_locations WHERE char_id = :p", [':p' => $id]) === false) { + throw new Exception('Failed to delete character location. (C::d)'); + } + + // Delete the character's gear + if (db_query(db_live(), "DELETE FROM char_gear WHERE char_id = :p", [':p' => $id]) === false) { + throw new Exception('Failed to delete character gear. (C::d)'); + } + + // Delete the character's bank + if (db_query(db_live(), "DELETE FROM char_bank WHERE char_id = :p", [':p' => $id]) === false) { + throw new Exception('Failed to delete character bank. (C::d)'); + } + + // Delete character's banked items + if (db_query(db_live(), "DELETE FROM char_banked_items WHERE char_id = :p", [':p' => $id]) === false) { + throw new Exception('Failed to delete character bank items. (C::d)'); + } + + // Delete the user's guild membership + if (db_query(db_live(), "DELETE FROM guild_members WHERE char_id = :p", [':p' => $id]) === false) { + throw new Exception('Failed to delete character guild membership. (C::d)'); + } + + // if the character was a guild leader, hand leadership to the next highest ranking member + $guild = db_query(db_live(), "SELECT id FROM guilds WHERE leader_id = :p", [':p' => $id])->fetchArray(SQLITE3_ASSOC); + if ($guild !== false) { + $members = db_query(db_live(), "SELECT char_id FROM guild_members WHERE guild_id = :p ORDER BY rank DESC", [':p' => $guild['id']]); + $newLeader = $members->fetchArray(SQLITE3_ASSOC); + if ($newLeader !== false) { + db_query(db_live(), "UPDATE guilds SET leader_id = :p WHERE id = :g", [':p' => $newLeader['char_id'], ':g' => $guild['id']]); + } + } + + // Get a list of all pve fight IDs. + $pve = db_query(db_fights(), "SELECT id FROM pve WHERE char_id = :p", [':p' => $id]); + // Get a list of all pvp fight IDs. + $pvp = db_query(db_fights(), "SELECT id FROM pvp WHERE char1_id = :p OR char2_id = :p", [':p' => $id]); + + // Delete all pve fights + while ($row = $pve->fetchArray(SQLITE3_ASSOC)) { + if (db_query(db_fights(), "DELETE FROM pve WHERE id = :p", [':p' => $row['id']]) === false) { + throw new Exception('Failed to delete pve fight. (C::d)'); + } + + if (db_query(db_fights(), "DELETE FROM pve_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) { + throw new Exception('Failed to delete pve fight logs. (C::d)'); + } + } + + // Delete all pvp fights + while ($row = $pvp->fetchArray(SQLITE3_ASSOC)) { + if (db_query(db_fights(), "DELETE FROM pvp WHERE id = :p", [':p' => $row['id']]) === false) { + throw new Exception('Failed to delete pvp fight. (C::d)'); + } + + if (db_query(db_fights(), "DELETE FROM pvp_logs WHERE fight_id = :p", [':p' => $row['id']]) === false) { + throw new Exception('Failed to delete pvp fight logs. (C::d)'); + } + } + } + + /** + * Get the character's current title. + */ + public function title(): array|false + { + $t = title($this->title_id); + + $q = db_query( + db_live(), + 'SELECT awarded FROM owned_titles WHERE char_id = :c AND title_id = :t LIMIT 1', + [':c' => $this->id, ':t' => $this->title_id] + ); + if ($q === false) throw new Exception('Failed to query title. (C::t)'); + + $a = $q->fetchArray(SQLITE3_ASSOC); + if ($a === false) return false; + + $t['awarded'] = $a['awarded']; // add the awarded date to the title info + return $t; + } +} diff --git a/src/model/fights.php b/src/models/fights.php similarity index 100% rename from src/model/fights.php rename to src/models/fights.php diff --git a/src/model/items.php b/src/models/items.php similarity index 100% rename from src/model/items.php rename to src/models/items.php diff --git a/src/model/session.php b/src/models/session.php similarity index 100% rename from src/model/session.php rename to src/models/session.php diff --git a/src/model/token.php b/src/models/token.php similarity index 100% rename from src/model/token.php rename to src/models/token.php diff --git a/src/models/user.php b/src/models/user.php new file mode 100644 index 0000000..b660cc7 --- /dev/null +++ b/src/models/user.php @@ -0,0 +1,180 @@ + $v) { + if (property_exists($this, $k)) { + $this->$k = in_array($k, ['created', 'last_login']) ? new DateTime($v) : $v; + } + } + } + + /** + * Find a user by their username, email or ID. Case-nonsensitive. Throws an Exception when the query is + * badly formed. Returns false if no user is found. Returns a User on success. + */ + public static function find(string|int $identifier): User|false + { + $r = db_query( + db_auth(), + "SELECT * FROM users WHERE username = :i COLLATE NOCASE OR email = :i COLLATE NOCASE OR id = :i LIMIT 1", + [':i' => $identifier] + ); + if ($r === false) throw new Exception("Failed to query user. (U::f)"); // badly formed query + $u = $r->fetchArray(SQLITE3_ASSOC); + if ($u === false) return false; // no user found + return new User($u); + } + + /** + * Create a new user row in the database. This function does not check for the unique-ness or validity + * of the username or password passed to it; that is the responsibility of the caller. Returns false on + * failure. + */ + public static function create(string $username, string $email, string $password, int $auth = 0): SQLite3Result|false + { + return db_query(db_auth(), "INSERT INTO users (username, email, password, auth) VALUES (:u, :e, :p, :a)", [ + ':u' => $username, + ':e' => $email, + ':p' => password_hash($password, PASSWORD_ARGON2ID), + ':a' => $auth + ]); + } + + /** + * Check $ref against the user's password. + */ + public function check_password(string $ref): bool + { + return password_verify($ref, $this->password); + } + + /** + * Check if the given username exists. + */ + public static function username_exists(string $username): bool + { + return db_exists(db_auth(), 'users', 'username', $username); + } + + /** + * Check if the given email exists. + */ + public static function email_exists(string $email): bool + { + return db_exists(db_auth(), 'users', 'email', $email); + } + + /** + * Delete a user by their username, email, or id. + */ + public static function delete(string|int $identifier): SQLite3Result|false + { + return db_query( + db_auth(), + "DELETE FROM users WHERE username = :i OR email = :i OR id = :i", + [':i' => $identifier] + ); + } + + /** + * Return a count of how many characters this user has. + */ + public function char_count(): int + { + $c = db_query( + db_live(), + "SELECT COUNT(*) FROM characters WHERE user_id = :u", + [':u' => $this->id] + )->fetchArray(SQLITE3_NUM); + if ($c === false) throw new Exception('Failed to count characters. (U::cc)'); + return (int) $c[0]; + } + + /** + * Get a an array of id => [name, level] for all characters associated with this user ID. Returns false + * if there are no characters. + */ + public function char_list(): array|false + { + $q = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = ?", [$this->id]); + if ($q === false) throw new Exception('Failed to list characters. (U->cl)'); + + $c = []; + while ($row = $q->fetchArray(SQLITE3_ASSOC)) { + $c[$row['id']] = ['name' => $row['name'], 'level' => $row['level']]; + } + + // return false if no characters + return empty($c) ? false : $c; + } + + /** + * Get the user's current Character. + */ + public function current_char(): Character|false + { + return Character::find($this->char_id); + } + + /** + * Get the user's wallet. + */ + public function wallet(): Wallet|false + { + return Wallet::find($this->id); + } +} diff --git a/src/models/wallet.php b/src/models/wallet.php new file mode 100644 index 0000000..b501ce2 --- /dev/null +++ b/src/models/wallet.php @@ -0,0 +1,52 @@ +fetchArray(SQLITE3_ASSOC); + if ($w === false) return false; // no wallet found + return new Wallet($user_id, $w['silver'], $w['stargem']); + } + + public static function create(int $user_id, int $silver = -1, int $starGems = -1): SQLite3Result|false + { + return db_query( + db_live(), + "INSERT INTO wallets (user_id, silver, stargem) VALUES (:u, :s, :sg)", + [ + ':u' => $user_id, + ':s' => $silver === -1 ? env('start_silver', 10) : $silver, + ':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems + ] + ); + } + + /** + * Add a certain amount of currency to the user's wallet. + */ + public function give(Currency $c, int $amt): SQLite3Result|false + { + $cs = $c->string(true); + $new = $this->{$cs} + $amt; + return db_query(db_live(), "UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new, $this->user_id]); + } + + /** + * Remove a certain amount of currency from the user's wallet. + */ + public function take(Currency $c, int $amt): SQLite3Result|false + { + $cs = $c->string(true); + $new = $this->{$cs} - $amt; + return db_query(db_live(), "UPDATE wallets SET $cs = ? WHERE user_id = ?", [$new < 0 ? 0 : $new, $this->user_id]); + } +} diff --git a/src/util/auth.php b/src/util/auth.php index bf7df2c..f90f2d7 100644 --- a/src/util/auth.php +++ b/src/util/auth.php @@ -1,21 +1,5 @@ char_id !== 0) { char(); return; } // if no characters, redirect to create first - if (char_count(user('id')) === 0) redirect('/character/create-first'); + if (user()->char_count() === 0) redirect('/character/create-first'); // if no character selected, select the first one - if ($_SESSION['user']['char_id'] === 0) { - $char = db_query(db_live(), 'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1', [':u' => user('id')])->fetchArray(SQLITE3_ASSOC); + if (user()->char_id === 0) { + $char = db_query( + db_live(), + 'SELECT * FROM characters WHERE user_id = :u ORDER BY id ASC LIMIT 1', + [':u' => user()->id] + )->fetchArray(SQLITE3_ASSOC); change_user_character($char['id']); } } diff --git a/src/util/components.php b/src/util/components.php index fd1ecf3..98dbd35 100644 --- a/src/util/components.php +++ b/src/util/components.php @@ -27,6 +27,14 @@ function c_left_nav() return render('components/left_nav'); } +/** + * Render the right sidebar menu. + */ +function c_right_nav() +{ + return render('components/right_nav', ['c' => char()]); +} + /** * Render the debug query log. */ diff --git a/src/util/database.php b/src/util/database.php index e7cb560..f80875e 100644 --- a/src/util/database.php +++ b/src/util/database.php @@ -57,10 +57,13 @@ function db_blueprints() * Take a SQLite3 database connection, a query string, and an array of parameters. Prepare the query and * bind the parameters with proper type casting. Then execute the query and return the result. */ -function db_query($db, $query, $params = []) +function db_query(SQLite3 $db, string $query, array $params = []): SQLite3Result|false { + $p = strpos($query, '?') !== false; // are generic placeholders? $stmt = $db->prepare($query); - if (!empty($params)) foreach ($params as $key => $value) $stmt->bindValue($key, $value, getSQLiteType($value)); + if (!empty($params)) { + foreach ($params as $k => $v) $stmt->bindValue($p ? $k + 1 : $k, $v, getSQLiteType($v)); + } $start = microtime(true); $r = $stmt->execute(); db_log($query, microtime(true) - $start); @@ -79,10 +82,10 @@ function db_exec($db, $query) } /** - * Take a SQLite3 database connection, a column name, and a value. Execute a COUNT query to see if the value + * Take a SQLite3 database connection, a column name, and a value. Execute a SELECT query to see if the value * exists in the column. Return true if the value exists, false otherwise. */ -function db_exists($db, $table, $column, $value, $caseInsensitive = true) +function db_exists(SQLite3 $db, string $table, string $column, mixed $value, bool $caseInsensitive = true): bool { if ($caseInsensitive) { $query = "SELECT 1 FROM $table WHERE $column = :v COLLATE NOCASE LIMIT 1"; diff --git a/src/util/enums.php b/src/util/enums.php new file mode 100644 index 0000000..f897a37 --- /dev/null +++ b/src/util/enums.php @@ -0,0 +1,25 @@ + $db ? 'silver' : 'Silver', + Currency::StarGem => $db ? 'stargem' : 'Star Gem' + }; + } +} diff --git a/templates/components/char_bar.php b/templates/components/char_bar.php deleted file mode 100644 index a883717..0000000 --- a/templates/components/char_bar.php +++ /dev/null @@ -1,36 +0,0 @@ -
-
-
- User - - 0): ?> - - -
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
- Silver -
-
-
diff --git a/templates/components/char_select_box.php b/templates/components/char_select_box.php index fb7aa39..e2e7bf6 100644 --- a/templates/components/char_select_box.php +++ b/templates/components/char_select_box.php @@ -1,5 +1,5 @@ -
- > +
+ char_id ? 'disabled' : '' ?>>