Compare commits
3 Commits
043a57cf86
...
95cee55d0c
Author | SHA1 | Date | |
---|---|---|---|
95cee55d0c | |||
e32803b7f9 | |||
a61ac11f60 |
1
.env
1
.env
|
@ -1,4 +1,5 @@
|
||||||
debug = true
|
debug = true
|
||||||
|
version = 0.1.0
|
||||||
open = true
|
open = true
|
||||||
world_size = 250
|
world_size = 250
|
||||||
exp_modifier = 1
|
exp_modifier = 1
|
||||||
|
|
BIN
database/auth.db
BIN
database/auth.db
Binary file not shown.
BIN
database/live.db
BIN
database/live.db
Binary file not shown.
98
docs/tooltip.md
Normal file
98
docs/tooltip.md
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
# Tooltip
|
||||||
|
|
||||||
|
The tooltip library is Isotip, taken and modified by Sharkk! Here is the breakdown of the API.
|
||||||
|
|
||||||
|
Configuring a specific tooltip is done via data attributes on an element.
|
||||||
|
|
||||||
|
### **`data-tooltip-classname`**
|
||||||
|
|
||||||
|
If you'd like to add a classname to the root tooltip element, set it here.
|
||||||
|
|
||||||
|
### **`data-tooltip-content`**
|
||||||
|
|
||||||
|
This sets the main body content of the tooltip into a `<p>` tag by default with a class of `tooltip-content`. Content is interpreted as plain text by default. To insert html, set the data-tooltip-html attribute to true.
|
||||||
|
|
||||||
|
### **`data-tooltip-title`**
|
||||||
|
|
||||||
|
This sets the title of the tooltip into a `<p>` tag by default with a class of `tooltip-title`.
|
||||||
|
|
||||||
|
### **`data-tooltip-html`**
|
||||||
|
|
||||||
|
Setting this to true will force Isotip to try and interpret the content as HTML. If it fails, it will interpret the content as plain text.
|
||||||
|
|
||||||
|
### **`data-tooltip-placement`**
|
||||||
|
|
||||||
|
This sets the position of the tooltip. Options are `top`, `right`, `bottom`, and `left`. By default, `top` is used for all tooltips.
|
||||||
|
|
||||||
|
### **`data-tooltip-container`**
|
||||||
|
|
||||||
|
This sets the element that the tooltip will be prepended to. By default, this is the `<body>` element.
|
||||||
|
|
||||||
|
Alternatively, programattic creation and destruction of tooltips is available.
|
||||||
|
|
||||||
|
### **`data-tooltip-scrollContainer`**
|
||||||
|
|
||||||
|
This sets the element that will have a scroll event bound to it. If your tooltip is inside a scrolling element (`overflow:scroll`), you need to add this!.
|
||||||
|
|
||||||
|
### **`data-tooltip-autoclose`**
|
||||||
|
|
||||||
|
If set to false, the tooltip will *not* close unless you do so programmatically with `isotip.close()`. Normal tooltips will not open until the open one has been closed!
|
||||||
|
|
||||||
|
### **`init( config )`**
|
||||||
|
|
||||||
|
The init method provides automatic event binding for tooltips. It sets up delegated event listeners for `.tooltip-click`, `.tooltip-hover`, and `.tooltip-focus` for click, mouseover, and focus events respectively. You can pass in an optional config object to overwrite any of the default options.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var options = {
|
||||||
|
html: false, // set to true to always interpret content as HTML
|
||||||
|
placement: 'top', // default placement of tooltips
|
||||||
|
container: 'body', // default containing element for the tooltip
|
||||||
|
scrollContainer: '.scroll-container', // default container for scroll watching
|
||||||
|
template: '<div class="tooltip" data-tooltip-target="tooltip"></div>', // default template for the tooltip shell
|
||||||
|
removalDelay: 200, // default number of ms before the tooltip is removed
|
||||||
|
tooltipOffset: 10, // default number of px the tooltip is offset from the trigger element
|
||||||
|
windowPadding: { // window bounds for tooltip repositioning
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 10,
|
||||||
|
left: 10
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Tooltip.init( config );
|
||||||
|
```
|
||||||
|
|
||||||
|
### **`open( trigger, config )`**
|
||||||
|
|
||||||
|
The open method will create the tooltip, insert it into the DOM, and position it in relation to it's trigger. The trigger can be an element or a CSS selector. The object to be passed in will serve as a replacement for the data attributes on the trigger.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var config = {
|
||||||
|
className: 'specific-class', // set to add a class to the tooltip
|
||||||
|
html: false, // set to true to interpret content as HTML
|
||||||
|
placement: 'top', // where to place the tooltip in relation to the trigger
|
||||||
|
content: 'Tooltip content', // the content to go into the tooltip,
|
||||||
|
title: 'Tooltip title', // the text to go in the title, if any
|
||||||
|
container: document.querySelector('.container'), // the container to append the tooltip to
|
||||||
|
scrollContainer: document.querySelector('.scroll-container'), // the container to bind the scroll event to
|
||||||
|
autoClose: false // set to false if you only want to close the tooltip programmatically. Normal tooltips won't open until the open one has been closed!
|
||||||
|
};
|
||||||
|
|
||||||
|
Tooltip.open( '.tooltip', config );
|
||||||
|
```
|
||||||
|
|
||||||
|
### **`close( tooltip )`**
|
||||||
|
|
||||||
|
The close method will remove a tooltip from the DOM. The tooltip to remove should be passed in and can be an element or a CSS selector.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
Tooltip.close( '.tooltip' );
|
||||||
|
```
|
||||||
|
|
||||||
|
### **`positionTooltip( tooltip, trigger, placement )`**
|
||||||
|
|
||||||
|
The positionTooltip method will re-evaluate the position of a tooltip in relation to it's trigger element. Only the tooltip and trigger need to be passed in, and placement will default to what's been configured by `init()`. Tooltip and trigger can be either an element or a CSS selector.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
Tooltip.positionTooltip( '.tooltip', '.tooltip-click', 'left' );
|
||||||
|
```
|
260
public/assets/css/dragon.css
Normal file
260
public/assets/css/dragon.css
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
@import '/assets/css/forms.css';
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #bcc6cf;
|
||||||
|
background-image: url('/assets/img/bg.jpg');
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-position: center top;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.button {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
border: none;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #f7f8fa linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
|
||||||
|
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
|
||||||
|
color: #111111;
|
||||||
|
padding: 0.5rem 1rem 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
user-select: none;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
background-image: linear-gradient(rgba(255, 255, 255, 0), rgba(0, 0, 0, 0.1));
|
||||||
|
box-shadow: 0 1px 0 1px rgba(255, 255, 255, 0.3) inset, 0 0 0 1px #adb2bb inset;
|
||||||
|
color: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 0.1rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: #f4cc67;
|
||||||
|
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
|
||||||
|
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
color: #111111;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #C59F43 #AA8326 #957321;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #fac847;
|
||||||
|
border-color: #C59F43 #AA8326 #957321;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background-color: #444c55;
|
||||||
|
color: #ffffff;
|
||||||
|
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #3D444C #2F353B #2C3137;
|
||||||
|
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #4e5964;
|
||||||
|
border-color: #32373E #24282D #212429;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 8fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside#left {
|
||||||
|
.box {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 0.15rem;
|
||||||
|
|
||||||
|
&#nav > a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.15rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
transition: color, background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
& > p:not(:last-child) {
|
||||||
|
margin-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#char-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
height: 34px;
|
||||||
|
color: white;
|
||||||
|
gap: 1rem;
|
||||||
|
background-image: url('/assets/img/deco-bar2.jpg');
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 18px;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.badge {
|
||||||
|
font-size: 10px;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
color: #111111;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.1rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-1 { margin-bottom: 0.25rem; margin-top: 0.25rem; }
|
||||||
|
.my-2 { margin-bottom: 0.5rem; margin-top: 0.5rem; }
|
||||||
|
.my-3 { margin-bottom: 0.75rem; margin-top: 0.75rem; }
|
||||||
|
.my-4 { margin-bottom: 1rem; margin-top: 1rem; }
|
||||||
|
|
||||||
|
.ml-1 { margin-left: 0.25rem; }
|
||||||
|
.ml-2 { margin-left: 0.5rem; }
|
||||||
|
.ml-3 { margin-left: 0.75rem; }
|
||||||
|
.ml-4 { margin-left: 1rem; }
|
||||||
|
|
||||||
|
.mr-1 { margin-right: 0.25rem; }
|
||||||
|
.mr-2 { margin-right: 0.5rem; }
|
||||||
|
.mr-3 { margin-right: 0.75rem; }
|
||||||
|
.mr-4 { margin-right: 1rem; }
|
||||||
|
|
||||||
|
.mb-1 { margin-bottom: 0.25rem; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem; }
|
||||||
|
.mb-3 { margin-bottom: 0.75rem; }
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.mt-1 { margin-top: 0.25rem; }
|
||||||
|
.mt-2 { margin-top: 0.5rem; }
|
||||||
|
.mt-3 { margin-top: 0.75rem; }
|
||||||
|
.mt-4 { margin-top: 1rem; }
|
||||||
|
|
||||||
|
|
||||||
|
.container-960 {
|
||||||
|
width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-meter {
|
||||||
|
background-color: black;
|
||||||
|
height: 16px;
|
||||||
|
min-width: 100px;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.hp {
|
||||||
|
background-color: #e57373;
|
||||||
|
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(139, 0, 0, 0.1));
|
||||||
|
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #d32f2f #c62828 #b71c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mp {
|
||||||
|
background-color: #5a9bd4;
|
||||||
|
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(60, 100, 150, 0.1));
|
||||||
|
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #4a8ab0 #3a7a9c #2a6a88;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tp {
|
||||||
|
background-color: #f4cc67;
|
||||||
|
background-image: linear-gradient(rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.1));
|
||||||
|
box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #C59F43 #AA8326 #957321;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
border: 1px solid #666;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-shadow: 0 0 0.5rem 0.1rem rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-trigger {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
23
public/assets/css/forms.css
Normal file
23
public/assets/css/forms.css
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.form.control {
|
||||||
|
appearance: none;
|
||||||
|
outline: none;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 34px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
color: #555555;
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||||
|
transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||||
|
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||||
|
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||||
|
}
|
BIN
public/assets/img/bg.jpg
Normal file
BIN
public/assets/img/bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
BIN
public/assets/img/deco-bar2.jpg
Normal file
BIN
public/assets/img/deco-bar2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.7 KiB |
BIN
public/assets/img/header.jpg
Normal file
BIN
public/assets/img/header.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
public/assets/img/icons/user1.png
Normal file
BIN
public/assets/img/icons/user1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
3
public/assets/scripts/tooltip.js
Normal file
3
public/assets/scripts/tooltip.js
Normal file
File diff suppressed because one or more lines are too long
10
public/assets/scripts/tooltip.js.map
Normal file
10
public/assets/scripts/tooltip.js.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -12,7 +12,8 @@ $r = [];
|
||||||
Home
|
Home
|
||||||
*/
|
*/
|
||||||
router_get($r, '/', function () {
|
router_get($r, '/', function () {
|
||||||
echo render('layouts/basic', ['view' => 'pages/home']);
|
if (user()) must_have_character();
|
||||||
|
echo render('layouts/basic', ['view' => 'pages/home', 'activeTab' => nav_tabs['home']]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -27,8 +28,10 @@ router_post($r, '/auth/logout', 'auth_controller_logout_post');
|
||||||
/*
|
/*
|
||||||
Characters
|
Characters
|
||||||
*/
|
*/
|
||||||
|
router_get($r, '/characters', 'char_controller_select_get');
|
||||||
router_post($r, '/character/create', 'char_controller_create_post');
|
router_post($r, '/character/create', 'char_controller_create_post');
|
||||||
router_post($r, '/character/select', 'auth_controller_change_character_post');
|
router_post($r, '/character/select', 'auth_controller_change_character_post');
|
||||||
|
router_get($r, '/character/create-first', 'char_controller_create_first_get');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Router
|
Router
|
||||||
|
|
48
src/auth.php
48
src/auth.php
|
@ -3,7 +3,7 @@
|
||||||
/**
|
/**
|
||||||
* Checks if the given username already exists.
|
* Checks if the given username already exists.
|
||||||
*/
|
*/
|
||||||
function auth_usernameExists(string $username): bool
|
function auth_username_exists(string $username): bool
|
||||||
{
|
{
|
||||||
return db_exists(db_auth(), 'users', 'username', $username);
|
return db_exists(db_auth(), 'users', 'username', $username);
|
||||||
}
|
}
|
||||||
|
@ -11,27 +11,11 @@ function auth_usernameExists(string $username): bool
|
||||||
/**
|
/**
|
||||||
* Checks if the given email already exists.
|
* Checks if the given email already exists.
|
||||||
*/
|
*/
|
||||||
function auth_emailExists(string $email): bool
|
function auth_email_exists(string $email): bool
|
||||||
{
|
{
|
||||||
return db_exists(db_auth(), 'users', 'email', $email);
|
return db_exists(db_auth(), 'users', 'email', $email);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a long-lived session for the user.
|
|
||||||
*/
|
|
||||||
function auth_rememberMe()
|
|
||||||
{
|
|
||||||
$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
|
|
||||||
]);
|
|
||||||
if (!$result) router_error(400);
|
|
||||||
set_cookie('remember_me', $token, $expires);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me
|
* Check for a user session. If $_SESSION['user'] already exists, return early. If not, check for a remember me
|
||||||
* cookie. If a remember me cookie exists, validate the session and set $_SESSION['user'].
|
* cookie. If a remember me cookie exists, validate the session and set $_SESSION['user'].
|
||||||
|
@ -46,6 +30,7 @@ function auth_check(): bool
|
||||||
$user = user_find($session['user_id']);
|
$user = user_find($session['user_id']);
|
||||||
unset($user['password']);
|
unset($user['password']);
|
||||||
$_SESSION['user'] = user_find($session['user_id']);
|
$_SESSION['user'] = user_find($session['user_id']);
|
||||||
|
$_SESSION['char'] = char_find($user['char_id']);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +42,7 @@ function auth_check(): bool
|
||||||
* Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and
|
* Ensure a user is logged in, or redirect to the login page. This will also check for a remember me cookie and
|
||||||
* populate the $_SESSION['user'] array.
|
* populate the $_SESSION['user'] array.
|
||||||
*/
|
*/
|
||||||
function auth_ensure(): void
|
function auth_only(): void
|
||||||
{
|
{
|
||||||
if (!auth_check()) redirect('/auth/login');
|
if (!auth_check()) redirect('/auth/login');
|
||||||
}
|
}
|
||||||
|
@ -65,7 +50,30 @@ function auth_ensure(): void
|
||||||
/**
|
/**
|
||||||
* If there is a user logged in, redirect to the home page. Used for when we have a guest-only page.
|
* If there is a user logged in, redirect to the home page. Used for when we have a guest-only page.
|
||||||
*/
|
*/
|
||||||
function auth_guest(): void
|
function guest_only(): void
|
||||||
{
|
{
|
||||||
if (auth_check()) redirect('/');
|
if (auth_check()) redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the user has a character selected. If they have no character, redirect to the character creation page. Otherwise,
|
||||||
|
* select the first character attached to the user.
|
||||||
|
*/
|
||||||
|
function must_have_character(): void
|
||||||
|
{
|
||||||
|
// If there is a character selected, make sure the session is up to date.
|
||||||
|
if ($_SESSION['user']['char_id'] !== 0) {
|
||||||
|
$char = db_query(db_live(), 'SELECT * FROM characters WHERE id = :c', [':c' => $_SESSION['user']['char_id']])->fetchArray(SQLITE3_ASSOC);
|
||||||
|
$_SESSION['char'] = $char;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no characters, redirect to create first
|
||||||
|
if (char_count(user('id')) === 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);
|
||||||
|
change_user_character($char['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ require_once SRC . '/env.php';
|
||||||
require_once SRC . '/database.php';
|
require_once SRC . '/database.php';
|
||||||
require_once SRC . '/auth.php';
|
require_once SRC . '/auth.php';
|
||||||
require_once SRC . '/router.php';
|
require_once SRC . '/router.php';
|
||||||
|
require_once SRC . '/components.php';
|
||||||
|
|
||||||
// Database models
|
// Database models
|
||||||
require_once SRC . '/models/user.php';
|
require_once SRC . '/models/user.php';
|
||||||
|
|
32
src/components.php
Normal file
32
src/components.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
const nav_tabs = [
|
||||||
|
'home' => 0,
|
||||||
|
'chars' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the logout button's form.
|
||||||
|
*/
|
||||||
|
function c_logout_button(): string
|
||||||
|
{
|
||||||
|
return render('components/logout_button');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the character bar. Relies on there being a character in the session. Without one, this will return an empty
|
||||||
|
* string.
|
||||||
|
*/
|
||||||
|
function c_char_bar(): string
|
||||||
|
{
|
||||||
|
if (!char()) return '';
|
||||||
|
return render('components/char_bar', ['char' => char()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the left sidebar navigation menu. Provide the active tab to highlight it.
|
||||||
|
*/
|
||||||
|
function c_left_nav(int $activeTab): string
|
||||||
|
{
|
||||||
|
return render('components/left_nav', ['activeTab' => $activeTab]);
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
function auth_controller_register_get(): void
|
function auth_controller_register_get(): void
|
||||||
{
|
{
|
||||||
auth_guest();
|
guest_only();
|
||||||
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
echo render('layouts/basic', ['view' => 'pages/auth/register']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ function auth_controller_register_get(): void
|
||||||
*/
|
*/
|
||||||
function auth_controller_register_post(): void
|
function auth_controller_register_post(): void
|
||||||
{
|
{
|
||||||
auth_guest();
|
guest_only();
|
||||||
csrf_ensure();
|
csrf_ensure();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
@ -63,14 +63,14 @@ function auth_controller_register_post(): void
|
||||||
/*
|
/*
|
||||||
A username must be unique.
|
A username must be unique.
|
||||||
*/
|
*/
|
||||||
if (auth_usernameExists($u)) {
|
if (auth_username_exists($u)) {
|
||||||
$errors['u'][] = 'Username is already taken.';
|
$errors['u'][] = 'Username is already taken.';
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
An email must be unique.
|
An email must be unique.
|
||||||
*/
|
*/
|
||||||
if (auth_emailExists($e)) {
|
if (auth_email_exists($e)) {
|
||||||
$errors['e'][] = 'Email is already taken.';
|
$errors['e'][] = 'Email is already taken.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ function auth_controller_register_post(): void
|
||||||
if ($user === false) router_error(400);
|
if ($user === false) router_error(400);
|
||||||
|
|
||||||
$_SESSION['user'] = user_find($u);
|
$_SESSION['user'] = user_find($u);
|
||||||
redirect('/');
|
redirect('/character/create-first');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -92,7 +92,7 @@ function auth_controller_register_post(): void
|
||||||
*/
|
*/
|
||||||
function auth_controller_login_get(): void
|
function auth_controller_login_get(): void
|
||||||
{
|
{
|
||||||
auth_guest();
|
guest_only();
|
||||||
echo render('layouts/basic', ['view' => 'pages/auth/login']);
|
echo render('layouts/basic', ['view' => 'pages/auth/login']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ function auth_controller_login_get(): void
|
||||||
*/
|
*/
|
||||||
function auth_controller_login_post(): void
|
function auth_controller_login_post(): void
|
||||||
{
|
{
|
||||||
auth_guest();
|
guest_only();
|
||||||
csrf_ensure();
|
csrf_ensure();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
@ -140,7 +140,20 @@ function auth_controller_login_post(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['user'] = $user;
|
$_SESSION['user'] = $user;
|
||||||
if ($_POST['remember'] ?? false) auth_rememberMe();
|
change_user_character($user['char_id']);
|
||||||
|
|
||||||
|
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
|
||||||
|
]);
|
||||||
|
if (!$result) router_error(400);
|
||||||
|
set_cookie('remember_me', $token, $expires);
|
||||||
|
}
|
||||||
|
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +165,7 @@ function auth_controller_logout_post(): void
|
||||||
csrf_ensure();
|
csrf_ensure();
|
||||||
session_delete($_SESSION['user']['id']);
|
session_delete($_SESSION['user']['id']);
|
||||||
unset($_SESSION['user']);
|
unset($_SESSION['user']);
|
||||||
|
unset($_SESSION['char']);
|
||||||
set_cookie('remember_me', '', 1);
|
set_cookie('remember_me', '', 1);
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
|
@ -161,16 +175,22 @@ function auth_controller_logout_post(): void
|
||||||
*/
|
*/
|
||||||
function auth_controller_change_character_post(): void
|
function auth_controller_change_character_post(): void
|
||||||
{
|
{
|
||||||
auth_check();
|
auth_only();
|
||||||
|
must_have_character();
|
||||||
csrf_ensure();
|
csrf_ensure();
|
||||||
|
|
||||||
$char_id = (int) ($_POST['char_id'] ?? 0);
|
$char_id = (int) ($_POST['char_id'] ?? 0);
|
||||||
if (char_exists($char_id) === false) router_error(400);
|
|
||||||
|
|
||||||
$_SESSION['user']['char_id'] = $char_id;
|
// If the character ID is the current character, do nothing.
|
||||||
if (db_query(db_auth(), 'UPDATE users SET char_id = :c WHERE id = :u', [
|
if ($char_id === $_SESSION['user']['char_id']) redirect('/');
|
||||||
':c' => $char_id,
|
|
||||||
':u' => $_SESSION['user']['id']
|
// Make sure the character ID is valid.
|
||||||
]) === false) router_error(400);
|
if (char_exists($char_id) === false) throw new Exception('Invalid character ID. (acccp)');
|
||||||
|
|
||||||
|
// Make sure the user owns the character.
|
||||||
|
if (char_belongs_to_user($char_id, $_SESSION['user']['id']) === false) router_error(999);
|
||||||
|
|
||||||
|
change_user_character($char_id);
|
||||||
|
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,37 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a player for the currently logged in user.
|
* Display a list of characters for the currently logged in user.
|
||||||
|
*/
|
||||||
|
function char_controller_select_get(): void
|
||||||
|
{
|
||||||
|
auth_only();
|
||||||
|
must_have_character();
|
||||||
|
|
||||||
|
$chars = char_list(user('id'));
|
||||||
|
|
||||||
|
echo render('layouts/basic', ['view' => 'pages/chars/select', 'chars' => $chars, 'activeTab' => nav_tabs['chars']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form to create your first character.
|
||||||
|
*/
|
||||||
|
function char_controller_create_first_get(): void
|
||||||
|
{
|
||||||
|
auth_only();
|
||||||
|
|
||||||
|
// If the user already has a character, redirect them to the main page.
|
||||||
|
if (char_count(user('id')) > 0) redirect('/');
|
||||||
|
|
||||||
|
echo render('layouts/basic', ['view' => 'pages/chars/first', 'activeTab' => nav_tabs['chars']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a character for the currently logged in user.
|
||||||
*/
|
*/
|
||||||
function char_controller_create_post(): void
|
function char_controller_create_post(): void
|
||||||
{
|
{
|
||||||
auth_ensure();
|
auth_only();
|
||||||
csrf_ensure();
|
csrf_ensure();
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
@ -25,9 +51,9 @@ function char_controller_create_post(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
A player's name must be unique.
|
A character's name must be unique.
|
||||||
*/
|
*/
|
||||||
if (char_nameExists($name)) $errors['name'][] = 'Name is already taken.';
|
if (char_name_exists($name)) $errors['name'][] = 'Name is already taken.';
|
||||||
|
|
||||||
// If there are errors at this point, send them to the page with errors flashed.
|
// If there are errors at this point, send them to the page with errors flashed.
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
|
@ -35,33 +61,17 @@ function char_controller_create_post(): void
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the player
|
// Create the character
|
||||||
$player = char_create(user('id'), $name);
|
$char = char_create(user('id'), $name);
|
||||||
if ($player === false) router_error(400);
|
if ($char === false) router_error(400);
|
||||||
|
|
||||||
// Create the auxiliary tables
|
// Create the auxiliary tables
|
||||||
char_location_create($player);
|
char_location_create($char);
|
||||||
char_wallet_create($player);
|
char_wallet_create($char);
|
||||||
char_gear_create($player);
|
char_gear_create($char);
|
||||||
|
|
||||||
redirect('/');
|
// Set the character as the user's selected character
|
||||||
}
|
change_user_character($char);
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the user's selected character.
|
|
||||||
*/
|
|
||||||
function char_controller_select_post(): void
|
|
||||||
{
|
|
||||||
auth_ensure();
|
|
||||||
csrf_ensure();
|
|
||||||
|
|
||||||
$char_id = (int) $_POST['char_id'] ?? 0;
|
|
||||||
|
|
||||||
// Ensure the character exists and belongs to the user
|
|
||||||
if (!char_exists($char_id)) router_error(400);
|
|
||||||
|
|
||||||
// Update the user's selected character
|
|
||||||
$_SESSION['user']['char_id'] = $char_id;
|
|
||||||
|
|
||||||
redirect('/');
|
redirect('/');
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
function env_load(string $filePath): void
|
function env_load(string $filePath): void
|
||||||
{
|
{
|
||||||
if (!file_exists($filePath)) throw new Exception("The .env file does not exist.");
|
if (!file_exists($filePath)) throw new Exception("The .env file does not exist. (el)");
|
||||||
|
|
||||||
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
|
|
|
@ -122,7 +122,38 @@ function user(string $field = ''): mixed
|
||||||
/**
|
/**
|
||||||
* Check whether the user has selected a character. If so, return the character's ID.
|
* Check whether the user has selected a character. If so, return the character's ID.
|
||||||
*/
|
*/
|
||||||
function char_selected(): int
|
function user_selected_char(): int
|
||||||
{
|
{
|
||||||
return (int) $_SESSION['user']['char_id'];
|
return (int) $_SESSION['user']['char_id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
function char(string $field = ''): mixed
|
||||||
|
{
|
||||||
|
if (empty($_SESSION['char'])) return false;
|
||||||
|
if ($field === '') return $_SESSION['char'];
|
||||||
|
return $_SESSION['char'][$field] ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand to update the user's selected character.
|
||||||
|
*/
|
||||||
|
function change_user_character(int $char_id): void
|
||||||
|
{
|
||||||
|
$_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')]);
|
||||||
|
$_SESSION['char'] = char_find($char_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a percent between two ints, rounded to the nearest whole number or return 0.
|
||||||
|
*/
|
||||||
|
function percent(int $num, int $denom, int $precision = 4): int
|
||||||
|
{
|
||||||
|
if ($denom === 0) return 0;
|
||||||
|
$p = ($num / $denom) * 100;
|
||||||
|
return $p < 0 ? 0 : round($p, $precision);
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Players are the living, breathing entities that interact with the game world. They are inextricably linked to their
|
characters are the living, breathing entities that interact with the game world. They are inextricably linked to their
|
||||||
accounts, and are the primary means by which the player interacts with the game world. Separating the player from
|
accounts, and are the primary means by which the character interacts with the game world. Separating the character from
|
||||||
the account allows for multiple players to be associated with a single account, and to prevent concurrency issues
|
the account allows for multiple characters to be associated with a single account, and to prevent concurrency issues
|
||||||
when performing auth checks on the database.
|
when performing auth checks on the database.
|
||||||
|
|
||||||
When creating a player, we want to init all of the related data tables; wallets, inventory, bank, etc.
|
When creating a character, we want to init all of the related data tables; wallets, inventory, bank, etc.
|
||||||
|
|
||||||
When retrieving a player, we will get the tables as-needed, to prevent allocating more memory than we need.
|
When retrieving a character, we will get the tables as-needed, to prevent allocating more memory than we need.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const currently = [
|
const currently = [
|
||||||
|
@ -20,9 +20,9 @@ const currently = [
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a player. Only a user ID and a name are required. All other fields are optional. Pass a key-value array
|
* 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 player's name must be unique, but this function does not check for
|
* of overrides to set additional fields. A character's name must be unique, but this function does not check for
|
||||||
* that. Returns the created player's ID.
|
* that. Returns the created character's ID.
|
||||||
*/
|
*/
|
||||||
function char_create(int $user_id, string $name, array $overrides = []): int
|
function char_create(int $user_id, string $name, array $overrides = []): int
|
||||||
{
|
{
|
||||||
|
@ -35,19 +35,19 @@ function char_create(int $user_id, string $name, array $overrides = []): int
|
||||||
$f = implode(', ', $k);
|
$f = implode(', ', $k);
|
||||||
$v = implode(', ', array_map(fn($x) => ":$x", $k));
|
$v = implode(', ', array_map(fn($x) => ":$x", $k));
|
||||||
|
|
||||||
// Create the player!
|
// Create the character!
|
||||||
if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
|
if (db_query(db_live(), "INSERT INTO characters ($f) VALUES ($v)", $data) === false) {
|
||||||
// @TODO: Log this error
|
// @TODO: Log this error
|
||||||
throw new Exception('Failed to create player.');
|
throw new Exception('Failed to create character. (cc)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the player ID
|
// Get the character ID
|
||||||
return db_live()->lastInsertRowID();
|
return db_live()->lastInsertRowID();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a player's location record. A player's location is where they are in the game world. A player can only be
|
* 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 player. Default state is 'Exploring'.
|
* in one location at a time. Can define a starting location for the character. Default state is 'Exploring'.
|
||||||
*/
|
*/
|
||||||
function char_location_create(int $char_id, int $x = 0, int $y = 0, int $currently = 0): void
|
function char_location_create(int $char_id, int $x = 0, int $y = 0, int $currently = 0): void
|
||||||
{
|
{
|
||||||
|
@ -57,12 +57,12 @@ function char_location_create(int $char_id, int $x = 0, int $y = 0, int $current
|
||||||
':y' => $y,
|
':y' => $y,
|
||||||
':c' => $currently
|
':c' => $currently
|
||||||
]) === false) {
|
]) === false) {
|
||||||
throw new Exception('Failed to create player location.');
|
throw new Exception('Failed to create character location. (clc)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a player's wallet. A player's wallet is where they store their currencies. Can optionally specify the
|
* Creates a character's wallet. A character's wallet is where they store their currencies. 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
|
* 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.
|
* or starting_star_gems fields from the env will be used.
|
||||||
*/
|
*/
|
||||||
|
@ -73,23 +73,23 @@ function char_wallet_create(int $char_id, int $silver = -1, int $starGems = -1):
|
||||||
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
|
':s' => $silver === -1 ? env('start_silver', 10) : $silver,
|
||||||
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
|
':sg' => $starGems === -1 ? env('start_star_gems', 0) : $starGems
|
||||||
]) === false) {
|
]) === false) {
|
||||||
throw new Exception('Failed to create player wallet.');
|
throw new Exception('Failed to create character wallet. (cwc)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the player's gear table. A player's gear is where they store their equipped items.
|
* Create the character's gear table. A character's gear is where they store their equipped items.
|
||||||
* @TODO: implement initial gear
|
* @TODO: implement initial gear
|
||||||
*/
|
*/
|
||||||
function char_gear_create(int $char_id, array $initialGear = []): void
|
function char_gear_create(int $char_id, array $initialGear = []): void
|
||||||
{
|
{
|
||||||
if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) {
|
if (db_query(db_live(), "INSERT INTO char_gear (char_id) VALUES (:p)", [':p' => $char_id]) === false) {
|
||||||
throw new Exception('Failed to create player gear.');
|
throw new Exception('Failed to create character gear. (cgc)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the player's bank account. The bank stores items and currency, with an interest rate based on
|
* Create the character's bank account. The bank stores items and currency, with an interest rate based on
|
||||||
* the bank account's tier. The bank account has a limited number of slots, which can be increased by upgrading
|
* the bank account's tier. The bank account has a limited number of slots, which can be increased by upgrading
|
||||||
* the bank account. The bank account starts with 0 silver and 5 slots.
|
* the bank account. The bank account starts with 0 silver and 5 slots.
|
||||||
*/
|
*/
|
||||||
|
@ -101,95 +101,73 @@ function char_bank_create(int $char_id, int $slots = 5, int $silver = 0, int $ti
|
||||||
':si' => $silver,
|
':si' => $silver,
|
||||||
':t' => $tier
|
':t' => $tier
|
||||||
]) === false) {
|
]) === false) {
|
||||||
throw new Exception('Failed to create player bank.');
|
throw new Exception('Failed to create character bank. (cbc)');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a player by their account ID. Returns the player's data as an associative array.
|
* Get a charcter by their ID. Returns the character's data as an associative array.
|
||||||
*/
|
*/
|
||||||
function char_find(int $char_id): array
|
function char_find(int $char_id): array
|
||||||
{
|
{
|
||||||
// Get the player
|
$char = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
||||||
$player = db_query(db_live(), "SELECT * FROM characters WHERE id = :id", [':id' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
if ($char === false) throw new Exception('Character not found. (cf)');
|
||||||
if ($player === false) {
|
return $char;
|
||||||
throw new Exception('Character not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $player;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count the number of players associated with an account ID.
|
* Count the number of characters associated with an account ID.
|
||||||
*/
|
*/
|
||||||
function char_count(int $user_id): int
|
function char_count(int $user_id): int
|
||||||
{
|
{
|
||||||
// Get the count
|
|
||||||
$count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM);
|
$count = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE user_id = :u", [':u' => $user_id])->fetchArray(SQLITE3_NUM);
|
||||||
if ($count === false) {
|
if ($count === false) throw new Exception('Failed to count characters. (cc)');
|
||||||
throw new Exception('Failed to count players.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) $count[0];
|
return (int) $count[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a an array of id => [name, level] for all players associated with an account ID.
|
* Get a an array of id => [name, level] for all characters associated with an account ID.
|
||||||
*/
|
*/
|
||||||
function char_list(int $user_id): array
|
function char_list(int $user_id): array
|
||||||
{
|
{
|
||||||
// Get the players
|
|
||||||
$stmt = db_query(db_live(), "SELECT id, name, level FROM characters WHERE user_id = :u", [':u' => $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 players.');
|
if ($stmt === false) throw new Exception('Failed to list characters. (cl)');
|
||||||
|
|
||||||
$players = [];
|
$characters = [];
|
||||||
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
|
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
|
||||||
$players[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
|
$characters[$row['id']] = ['name' => $row['name'], 'level' => $row['level']];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $players;
|
return $characters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a player's location info by their player ID. Returns the location's data as an associative array.
|
* Get a character's location info by their character ID. Returns the location's data as an associative array.
|
||||||
*/
|
*/
|
||||||
function char_get_location(int $char_id): array
|
function char_get_location(int $char_id): array
|
||||||
{
|
{
|
||||||
// Get the location
|
// Get the location
|
||||||
$location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
$location = db_query(db_live(), "SELECT * FROM char_locations WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
||||||
if ($location === false) {
|
if ($location === false) throw new Exception('Location not found. (cgl)');
|
||||||
throw new Exception('Location not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $location;
|
return $location;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a player's wallet by their player ID. Returns the wallet's data as an associative array.
|
* Get a character's wallet by their character ID. Returns the wallet's data as an associative array.
|
||||||
*/
|
*/
|
||||||
function char_get_wallet(int $char_id): array
|
function char_get_wallet(int $char_id): array
|
||||||
{
|
{
|
||||||
// Get the wallet
|
|
||||||
$wallet = db_query(db_live(), "SELECT * FROM char_wallets WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
$wallet = db_query(db_live(), "SELECT * FROM char_wallets WHERE char_id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
||||||
if ($wallet === false) {
|
if ($wallet === false) throw new Exception('Wallet not found. (cgw)');
|
||||||
throw new Exception('Wallet not found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $wallet;
|
return $wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See if a player name exists.
|
* See if a character name exists.
|
||||||
*/
|
*/
|
||||||
function char_nameExists(string $name): bool
|
function char_name_exists(string $name): bool
|
||||||
{
|
{
|
||||||
// Check for the name
|
return db_exists(db_live(), 'characters', 'name', $name);
|
||||||
$exists = db_query(db_live(), "SELECT COUNT(*) FROM characters WHERE name = :n", [':n' => $name])->fetchArray(SQLITE3_NUM);
|
|
||||||
if ($exists === false) {
|
|
||||||
throw new Exception('Failed to check for player name.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int) $exists[0] > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -199,3 +177,13 @@ function char_exists(int $char_id): bool
|
||||||
{
|
{
|
||||||
return db_exists(db_live(), 'characters', 'id', $char_id);
|
return db_exists(db_live(), 'characters', 'id', $char_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See if the given character belongs to the given user.
|
||||||
|
*/
|
||||||
|
function char_belongs_to_user(int $char_id, int $user_id): bool
|
||||||
|
{
|
||||||
|
$char = db_query(db_live(), "SELECT user_id FROM characters WHERE id = :p", [':p' => $char_id])->fetchArray(SQLITE3_ASSOC);
|
||||||
|
if ($char === false) throw new Exception('Character not found. (cbtu)');
|
||||||
|
return $char['user_id'] === $user_id;
|
||||||
|
}
|
||||||
|
|
|
@ -124,6 +124,7 @@ function router_error(int $code): void
|
||||||
404 => 'Not Found',
|
404 => 'Not Found',
|
||||||
405 => 'Method Not Allowed',
|
405 => 'Method Not Allowed',
|
||||||
418 => 'I\'m a teapot',
|
418 => 'I\'m a teapot',
|
||||||
|
999 => 'Cheating attempt detected',
|
||||||
default => 'Unknown Error',
|
default => 'Unknown Error',
|
||||||
};
|
};
|
||||||
exit;
|
exit;
|
||||||
|
|
30
templates/components/char_bar.php
Normal file
30
templates/components/char_bar.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<div id="char-bar">
|
||||||
|
<div>
|
||||||
|
<img class="icon" src="/assets/img/icons/user1.png" alt="User">
|
||||||
|
<?= $char['name'] ?> <span class="badge ml-2 tooltip-hover" data-tooltip-content="Level"><?= $char['level'] ?></span>
|
||||||
|
<?php if ($char['attrib_points'] > 0): ?>
|
||||||
|
<span class="ui button primary badge ml-2 tooltip-hover" data-tooltip-content="Attribute Points"><?= $char['attrib_points'] ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="char-meter">
|
||||||
|
<div class="hp" style="width: <?= percent($char['current_hp'], $char['max_hp']) ?>%"></div>
|
||||||
|
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Health<br><?= $char['current_hp'] ?> / <?= $char['max_hp'] ?>"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="char-meter">
|
||||||
|
<div class="mp" style="width: <?= percent($char['current_mp'], $char['max_mp']) ?>%"></div>
|
||||||
|
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Mana<br><?= $char['current_mp'] ?> / <?= $char['max_mp'] ?>"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="char-meter">
|
||||||
|
<div class="tp" style="width: <?= percent($char['current_tp'], $char['max_tp']) ?>%"></div>
|
||||||
|
<div class="tooltip-trigger tooltip-hover" data-tooltip-content="Travel Points<br><?= $char['current_tp'] ?> / <?= $char['max_tp'] ?>"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
4
templates/components/left_nav.php
Normal file
4
templates/components/left_nav.php
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<div id="nav" class="box">
|
||||||
|
<a href="/" class="<?= $activeTab === 0 ? 'active' : '' ?>">Home</a>
|
||||||
|
<a href="/characters" class="<?= $activeTab === 1 ? 'active' : '' ?>">Characters</a>
|
||||||
|
</div>
|
4
templates/components/logout_button.php
Normal file
4
templates/components/logout_button.php
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<form action="/auth/logout" method="post">
|
||||||
|
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
||||||
|
<input type="submit" value="Logout" class="ui button secondary">
|
||||||
|
</form>
|
0
templates/components/right_nav.php
Normal file
0
templates/components/right_nav.php
Normal file
|
@ -4,20 +4,54 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Dragon Knight</title>
|
<title>Dragon Knight</title>
|
||||||
|
<link rel="stylesheet" href="/assets/css/dragon.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="dk">
|
<header>
|
||||||
<header>
|
<div class="left">
|
||||||
<h1>Dragon Knight</h1>
|
<h1>Dragon Knight</h1>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
<main>
|
<div class="right">
|
||||||
|
<?php if (user()): ?>
|
||||||
|
<p>Welcome, <?= user('username') ?></p>
|
||||||
|
<?= c_logout_button() ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<a class="ui button primary" href="/auth/login">Login</a>
|
||||||
|
<a class="ui button secondary" href="/auth/register">Register</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<?= c_char_bar(user('char_id')) ?>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<aside id="left">
|
||||||
|
<?php if (user()): ?>
|
||||||
|
<?= c_left_nav($activeTab ?? 0) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div id="center">
|
||||||
<?= render($view, $data) ?>
|
<?= render($view, $data) ?>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<aside id="right">
|
||||||
<p>© 2024 Dragon Knight</p>
|
<?php if (user()): ?>
|
||||||
</footer>
|
// right nav
|
||||||
</div>
|
<?php endif; ?>
|
||||||
|
</aside>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© <?= date('Y') ?> Dragon Knight</p>
|
||||||
|
<p>q<?= $GLOBALS['queries'] ?></p>
|
||||||
|
<p>v<?= env('version') ?></p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import Tooltip from '/assets/scripts/tooltip.js';
|
||||||
|
Tooltip.init();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -9,11 +9,21 @@
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<form action="/auth/login" method="post">
|
<div class="container-960">
|
||||||
<?= csrf_field() ?>
|
<h1 class="my-4">Login</h1>
|
||||||
|
|
||||||
|
<form action="/auth/login" method="post">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<input class="form control mb-1" type="text" name="username" placeholder="Username">
|
||||||
|
<input class="form control mb-4" type="password" name="password" placeholder="Password">
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<input type="checkbox" name="remember" id="remember"> <label for="remember">remember me</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="ui button primary" type="submit">Login</button>
|
||||||
|
<a href="/auth/register" class="ui button secondary">Register</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="text" name="username" placeholder="Username">
|
|
||||||
<input type="password" name="password" placeholder="Password">
|
|
||||||
<input type="checkbox" name="remember" id="remember"> <label for="remember">remember me</label>
|
|
||||||
<input type="submit" value="Login">
|
|
||||||
</form>
|
|
||||||
|
|
|
@ -9,11 +9,17 @@
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<form action="/auth/register" method="post">
|
<div class="container-960">
|
||||||
<?= csrf_field() ?>
|
<h1 class="my-4">Register</h1>
|
||||||
|
<form action="/auth/register" method="post">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<input type="text" name="username" placeholder="Username" class="form control mb-1">
|
||||||
|
<input type="text" name="email" placeholder="Email" class="form control mb-1">
|
||||||
|
<input type="password" name="password" placeholder="Password" class="form control mb-4">
|
||||||
|
|
||||||
|
<button type="submit" class="ui button primary">Register</button>
|
||||||
|
<a href="/auth/login" class="ui button secondary">Login</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="text" name="username" placeholder="Username">
|
|
||||||
<input type="text" name="email" placeholder="Email">
|
|
||||||
<input type="password" name="password" placeholder="Password">
|
|
||||||
<input type="submit" value="Register">
|
|
||||||
</form>
|
|
||||||
|
|
30
templates/pages/chars/first.php
Normal file
30
templates/pages/chars/first.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
$errors = flash('errors');
|
||||||
|
if ($errors !== false) {
|
||||||
|
foreach ($errors as $error) {
|
||||||
|
foreach ($error as $message) {
|
||||||
|
echo "<p>$message</p>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="container-960">
|
||||||
|
<h1 class="my-4">Create Your First Character</h1>
|
||||||
|
|
||||||
|
<p class="mb-2">Welcome to Dragon Knight!</p>
|
||||||
|
|
||||||
|
<p class="mb-4">
|
||||||
|
Before you can begin your adventure, you need to make your first character. Pick a name below. You
|
||||||
|
can create multiple characters later, and there are no classes; feel free to experiment!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action="/character/create" method="post">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
|
||||||
|
<input class="form control mb-2" type="text" name="name" placeholder="Character Name">
|
||||||
|
|
||||||
|
<button class="ui button primary" type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
16
templates/pages/chars/select.php
Normal file
16
templates/pages/chars/select.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<h1>Characters</h1>
|
||||||
|
<?php
|
||||||
|
$list = char_list(user('id'));
|
||||||
|
if (count($list) > 0): ?>
|
||||||
|
<form action="/character/select" method="post">
|
||||||
|
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
||||||
|
<?php foreach ($list as $id => $char): ?>
|
||||||
|
<input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>">
|
||||||
|
<label for="char_<?= $id ?>"><?= $char['name'] ?> (Level <?= $char['level'] ?>)</label><br>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<input type="submit" value="Select Character">
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- Should never see this particular message. If you have, there's a bug. -->
|
||||||
|
<p>You have no characters.</p>
|
||||||
|
<?php endif; ?>
|
|
@ -1,32 +1,8 @@
|
||||||
<?php if (!user()): ?>
|
<?php if (!user()): ?>
|
||||||
<h2>Welcome!</h2>
|
<h1>Welcome!</h1>
|
||||||
<a href="/auth/register">Register</a>
|
<a href="/auth/login" class="ui button primary">Login</a>
|
||||||
<a href="/auth/login">Login</a>
|
<a href="/auth/register" class="ui button secondary">Register</a>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<h2>Hello, <?= user('username') ?>!</h2>
|
<h1 class="tooltip-click" data-tooltip-content="Hover-based tooltip">Home</h1>
|
||||||
<?php if (user('char_id') !== 0): ?>
|
<?= print_r(char()) ?>
|
||||||
<h3>Playing as <?= char_find(user('char_id'))['name'] ?></h3>
|
|
||||||
<?php endif; ?>
|
|
||||||
<form action="/auth/logout" method="post">
|
|
||||||
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
|
||||||
<input type="submit" value="Logout">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if (char_count(user('id')) > 0): ?>
|
|
||||||
<h3>Characters</h3>
|
|
||||||
<form action="character/select" method="post">
|
|
||||||
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
|
||||||
<?php foreach (char_list(user('id')) as $id => $char): ?>
|
|
||||||
<input type="radio" name="char_id" value="<?= $id ?>" id="char_<?= $id ?>">
|
|
||||||
<label for="char_<?= $id ?>"><?= $char['name'] ?> (Level <?= $char['level'] ?>)</label><br>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<input type="submit" value="Select Character">
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<form action="/character/create" method="post">
|
|
||||||
<input type="hidden" name="csrf" value="<?= csrf() ?>">
|
|
||||||
<input type="text" name="name" placeholder="Character Name">
|
|
||||||
<input type="submit" value="Create Character">
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user