improve database usage with models and change trackers, work on statbars
This commit is contained in:
parent
85af81a818
commit
f0756c79b6
@ -112,6 +112,7 @@ div.title {
|
|||||||
background-color: #eeeeee;
|
background-color: #eeeeee;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
@ -264,3 +265,49 @@ button.img-button {
|
|||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#statbars {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
& > div.stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& > div.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
width: 16px;
|
||||||
|
height: 160px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
|
& > div.bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&#hp > div.bar {
|
||||||
|
background: #56ab2f;
|
||||||
|
background: linear-gradient(to left, #a8e063, #56ab2f);
|
||||||
|
}
|
||||||
|
|
||||||
|
&#mp > div.bar {
|
||||||
|
background: #00c6ff;
|
||||||
|
background: linear-gradient(to right, #0072ff, #00c6ff);
|
||||||
|
}
|
||||||
|
|
||||||
|
&#tp > div.bar {
|
||||||
|
background: #757f9a;
|
||||||
|
background: linear-gradient(to right, #757f9a, #d7dde8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > label {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,9 +25,9 @@ func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*use
|
|||||||
var user *users.User
|
var user *users.User
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
user, err = users.GetByUsername(usernameOrEmail)
|
user, err = users.ByUsername(usernameOrEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
user, err = users.GetByEmail(usernameOrEmail)
|
user, err = users.ByEmail(usernameOrEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
122
internal/database/model.go
Normal file
122
internal/database/model.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"zombiezen.com/go/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Model interface for trackable database models
|
||||||
|
type Model interface {
|
||||||
|
GetTableName() string
|
||||||
|
GetID() int
|
||||||
|
SetID(id int)
|
||||||
|
GetDirtyFields() map[string]any
|
||||||
|
SetDirty(field string, value any)
|
||||||
|
ClearDirty()
|
||||||
|
IsDirty() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseModel provides common model functionality
|
||||||
|
type BaseModel struct {
|
||||||
|
FieldTracker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set uses reflection to set a field and track changes
|
||||||
|
func Set(model Model, field string, value any) error {
|
||||||
|
v := reflect.ValueOf(model).Elem()
|
||||||
|
fieldVal := v.FieldByName(field)
|
||||||
|
|
||||||
|
if !fieldVal.IsValid() {
|
||||||
|
return fmt.Errorf("field %s does not exist", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fieldVal.CanSet() {
|
||||||
|
return fmt.Errorf("field %s cannot be set", field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current value for comparison
|
||||||
|
currentVal := fieldVal.Interface()
|
||||||
|
|
||||||
|
// Only set if value has changed
|
||||||
|
if !reflect.DeepEqual(currentVal, value) {
|
||||||
|
// Convert value to correct type
|
||||||
|
newVal := reflect.ValueOf(value)
|
||||||
|
if newVal.Type().ConvertibleTo(fieldVal.Type()) {
|
||||||
|
fieldVal.Set(newVal.Convert(fieldVal.Type()))
|
||||||
|
|
||||||
|
// Convert field name to snake_case for database
|
||||||
|
dbField := toSnakeCase(field)
|
||||||
|
model.SetDirty(dbField, value)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("cannot convert %T to %s", value, fieldVal.Type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSnakeCase converts CamelCase to snake_case
|
||||||
|
func toSnakeCase(s string) string {
|
||||||
|
var result strings.Builder
|
||||||
|
for i, r := range s {
|
||||||
|
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||||
|
result.WriteByte('_')
|
||||||
|
}
|
||||||
|
if r >= 'A' && r <= 'Z' {
|
||||||
|
result.WriteRune(r - 'A' + 'a')
|
||||||
|
} else {
|
||||||
|
result.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updates only dirty fields
|
||||||
|
func Save(model Model) error {
|
||||||
|
if model.GetID() == 0 {
|
||||||
|
return fmt.Errorf("cannot save model without ID")
|
||||||
|
}
|
||||||
|
return UpdateDirty(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert creates a new record and sets the ID
|
||||||
|
func Insert(model Model, columns string, values ...any) error {
|
||||||
|
if model.GetID() != 0 {
|
||||||
|
return fmt.Errorf("model already has ID %d, use Save() to update", model.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transaction(func(tx *Tx) error {
|
||||||
|
placeholders := strings.Repeat("?,", len(values))
|
||||||
|
placeholders = placeholders[:len(placeholders)-1] // Remove trailing comma
|
||||||
|
|
||||||
|
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||||
|
model.GetTableName(), columns, placeholders)
|
||||||
|
|
||||||
|
if err := tx.Exec(query, values...); err != nil {
|
||||||
|
return fmt.Errorf("failed to insert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var id int
|
||||||
|
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
|
||||||
|
id = stmt.ColumnInt(0)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get insert ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
model.SetID(id)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the record
|
||||||
|
func Delete(model Model) error {
|
||||||
|
if model.GetID() == 0 {
|
||||||
|
return fmt.Errorf("cannot delete model without ID")
|
||||||
|
}
|
||||||
|
return Exec("DELETE FROM ? WHERE id = ?", model.GetTableName(), model.GetID())
|
||||||
|
}
|
82
internal/database/trackable.go
Normal file
82
internal/database/trackable.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Trackable interface for models that can track field changes
|
||||||
|
type Trackable interface {
|
||||||
|
GetTableName() string
|
||||||
|
GetID() int
|
||||||
|
GetDirtyFields() map[string]any
|
||||||
|
SetDirty(field string, value any)
|
||||||
|
ClearDirty()
|
||||||
|
IsDirty() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldTracker provides dirty field tracking functionality
|
||||||
|
type FieldTracker struct {
|
||||||
|
dirty map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDirty marks a field as dirty with its new value
|
||||||
|
func (ft *FieldTracker) SetDirty(field string, value any) {
|
||||||
|
if ft.dirty == nil {
|
||||||
|
ft.dirty = make(map[string]any)
|
||||||
|
}
|
||||||
|
ft.dirty[field] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDirtyFields returns map of dirty fields and their values
|
||||||
|
func (ft *FieldTracker) GetDirtyFields() map[string]any {
|
||||||
|
if ft.dirty == nil {
|
||||||
|
return make(map[string]any)
|
||||||
|
}
|
||||||
|
return ft.dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearDirty clears all dirty field tracking
|
||||||
|
func (ft *FieldTracker) ClearDirty() {
|
||||||
|
ft.dirty = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDirty returns true if any fields have been modified
|
||||||
|
func (ft *FieldTracker) IsDirty() bool {
|
||||||
|
return len(ft.dirty) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDirty updates only dirty fields in the database
|
||||||
|
func UpdateDirty(model Trackable) error {
|
||||||
|
if !model.IsDirty() {
|
||||||
|
return nil // No changes to save
|
||||||
|
}
|
||||||
|
|
||||||
|
dirty := model.GetDirtyFields()
|
||||||
|
if len(dirty) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build dynamic UPDATE query
|
||||||
|
var setParts []string
|
||||||
|
var args []any
|
||||||
|
|
||||||
|
for field, value := range dirty {
|
||||||
|
setParts = append(setParts, field+" = ?")
|
||||||
|
args = append(args, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, model.GetID()) // Add ID for WHERE clause
|
||||||
|
|
||||||
|
query := fmt.Sprintf("UPDATE %s SET %s WHERE id = ?",
|
||||||
|
model.GetTableName(),
|
||||||
|
strings.Join(setParts, ", "))
|
||||||
|
|
||||||
|
err := Exec(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update %s: %w", model.GetTableName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
model.ClearDirty()
|
||||||
|
return nil
|
||||||
|
}
|
10
internal/helpers/math.go
Normal file
10
internal/helpers/math.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package helpers
|
||||||
|
|
||||||
|
// ClampPct divides two floats and clamps them to the given limits. Provides
|
||||||
|
// divide-by-zero protection by just returning 0.
|
||||||
|
func ClampPct(num, denom, minimum, maximum float64) float64 {
|
||||||
|
if denom == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return max(minimum, min(maximum, (num/denom)*100))
|
||||||
|
}
|
@ -46,6 +46,10 @@ func RequireAuth(paths ...string) router.Middleware {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user := ctx.UserValue("user").(*users.User)
|
||||||
|
user.UpdateLastOnline()
|
||||||
|
user.Save()
|
||||||
|
|
||||||
next(ctx, params)
|
next(ctx, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ func processRegister(ctx router.Ctx, _ []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := users.GetByUsername(username); err == nil {
|
if _, err := users.ByUsername(username); err == nil {
|
||||||
auth.SetFlashMessage(ctx, "error", "Username already exists")
|
auth.SetFlashMessage(ctx, "error", "Username already exists")
|
||||||
auth.SetFormData(ctx, map[string]string{
|
auth.SetFormData(ctx, map[string]string{
|
||||||
"username": username,
|
"username": username,
|
||||||
@ -153,7 +153,7 @@ func processRegister(ctx router.Ctx, _ []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := users.GetByEmail(email); err == nil {
|
if _, err := users.ByEmail(email); err == nil {
|
||||||
auth.SetFlashMessage(ctx, "error", "Email already registered")
|
auth.SetFlashMessage(ctx, "error", "Email already registered")
|
||||||
auth.SetFormData(ctx, map[string]string{
|
auth.SetFormData(ctx, map[string]string{
|
||||||
"username": username,
|
"username": username,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package components
|
package components
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"dk/internal/helpers"
|
||||||
"dk/internal/middleware"
|
"dk/internal/middleware"
|
||||||
"dk/internal/router"
|
"dk/internal/router"
|
||||||
"dk/internal/template"
|
"dk/internal/template"
|
||||||
@ -61,31 +62,24 @@ func LeftAside(ctx router.Ctx) string {
|
|||||||
|
|
||||||
// RightAside generates the right sidebar content
|
// RightAside generates the right sidebar content
|
||||||
func RightAside(ctx router.Ctx) string {
|
func RightAside(ctx router.Ctx) string {
|
||||||
if !middleware.IsAuthenticated(ctx) {
|
user := middleware.GetCurrentUser(ctx)
|
||||||
|
if user == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load and render the rightside template with user data
|
tmpl, err := template.Cache.Load("rightside.html")
|
||||||
rightSideTmpl, err := template.Cache.Load("rightside.html")
|
|
||||||
if err != nil {
|
|
||||||
return "" // Silently fail - sidebar is optional
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current user from session
|
|
||||||
currentUser := middleware.GetCurrentUser(ctx)
|
|
||||||
if currentUser == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := users.Find(currentUser.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the user object directly to the template
|
hpPct := helpers.ClampPct(float64(user.HP), float64(user.MaxHP), 0, 100)
|
||||||
rightSideData := map[string]any{
|
mpPct := helpers.ClampPct(float64(user.MP), float64(user.MaxMP), 0, 100)
|
||||||
|
tpPct := helpers.ClampPct(float64(user.TP), float64(user.MaxTP), 0, 100)
|
||||||
|
|
||||||
|
return tmpl.RenderNamed(map[string]any{
|
||||||
"user": user.ToMap(),
|
"user": user.ToMap(),
|
||||||
}
|
"hppct": hpPct,
|
||||||
|
"mppct": mpPct,
|
||||||
return rightSideTmpl.RenderNamed(rightSideData)
|
"tppct": tpPct,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@ package components
|
|||||||
import (
|
import (
|
||||||
"dk/internal/helpers/markdown"
|
"dk/internal/helpers/markdown"
|
||||||
"dk/internal/news"
|
"dk/internal/news"
|
||||||
|
"dk/internal/users"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateTownNews() string {
|
func GenerateTownNews() string {
|
||||||
@ -22,5 +24,19 @@ func GenerateTownNews() string {
|
|||||||
func GenerateTownWhosOnline() string {
|
func GenerateTownWhosOnline() string {
|
||||||
title := `<div class="title">Who's Online</div>`
|
title := `<div class="title">Who's Online</div>`
|
||||||
|
|
||||||
|
onlineUsers, err := users.Online(10 * time.Minute)
|
||||||
|
if err == nil && len(onlineUsers) > 0 {
|
||||||
|
if len(onlineUsers) == 1 {
|
||||||
|
title += "<p>There is 1 user online in the last 10 minutes:</p>"
|
||||||
|
} else {
|
||||||
|
title += fmt.Sprintf("<p>There are %d users online in the last 10 minutes:</p>", len(onlineUsers))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range onlineUsers {
|
||||||
|
title += fmt.Sprintf(`<div>%s</div>`, user.Username)
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
||||||
return title + "<div>No one!</div>"
|
return title + "<div>No one!</div>"
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
|
|
||||||
// User represents a user in the database
|
// User represents a user in the database
|
||||||
type User struct {
|
type User struct {
|
||||||
|
database.BaseModel
|
||||||
|
|
||||||
ID int `db:"id" json:"id"`
|
ID int `db:"id" json:"id"`
|
||||||
Username string `db:"username" json:"username"`
|
Username string `db:"username" json:"username"`
|
||||||
Password string `db:"password" json:"password"`
|
Password string `db:"password" json:"password"`
|
||||||
@ -65,296 +67,221 @@ type User struct {
|
|||||||
Towns string `db:"towns" json:"towns"`
|
Towns string `db:"towns" json:"towns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implement Model interface
|
||||||
|
func (u *User) GetTableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) GetID() int {
|
||||||
|
return u.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) SetID(id int) {
|
||||||
|
u.ID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods wrapping generic functions
|
||||||
|
func (u *User) Set(field string, value any) error {
|
||||||
|
return database.Set(u, field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Save() error {
|
||||||
|
return database.Save(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Delete() error {
|
||||||
|
return database.Delete(u)
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a new User with sensible defaults
|
// New creates a new User with sensible defaults
|
||||||
func New() *User {
|
func New() *User {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
return &User{
|
return &User{
|
||||||
Verified: 0, // Default unverified
|
Verified: 0,
|
||||||
Token: "", // Empty verification token
|
Token: "",
|
||||||
Registered: now, // Current time
|
Registered: now,
|
||||||
LastOnline: now, // Current time
|
LastOnline: now,
|
||||||
Auth: 0, // Default no special permissions
|
Auth: 0,
|
||||||
X: 0, // Default starting position
|
X: 0,
|
||||||
Y: 0, // Default starting position
|
Y: 0,
|
||||||
ClassID: 1, // Default to class 1
|
ClassID: 1,
|
||||||
Currently: "In Town", // Default status
|
Currently: "In Town",
|
||||||
Fighting: 0, // Default not fighting
|
Fighting: 0,
|
||||||
HP: 15, // Default starting HP
|
HP: 15,
|
||||||
MP: 0, // Default starting MP
|
MP: 0,
|
||||||
TP: 10, // Default starting TP
|
TP: 10,
|
||||||
MaxHP: 15, // Default starting max HP
|
MaxHP: 15,
|
||||||
MaxMP: 0, // Default starting max MP
|
MaxMP: 0,
|
||||||
MaxTP: 10, // Default starting max TP
|
MaxTP: 10,
|
||||||
Level: 1, // Default starting level
|
Level: 1,
|
||||||
Gold: 100, // Default starting gold
|
Gold: 100,
|
||||||
Exp: 0, // Default starting exp
|
Exp: 0,
|
||||||
Strength: 5, // Default starting strength
|
Strength: 5,
|
||||||
Dexterity: 5, // Default starting dexterity
|
Dexterity: 5,
|
||||||
Attack: 5, // Default starting attack
|
Attack: 5,
|
||||||
Defense: 5, // Default starting defense
|
Defense: 5,
|
||||||
Spells: "", // No spells initially
|
Spells: "",
|
||||||
Towns: "", // No towns visited initially
|
Towns: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var userScanner = scanner.New[User]()
|
var userScanner = scanner.New[User]()
|
||||||
|
|
||||||
// userColumns returns the column list for user queries
|
|
||||||
func userColumns() string {
|
func userColumns() string {
|
||||||
return userScanner.Columns()
|
return userScanner.Columns()
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanUser populates a User struct using the fast scanner
|
|
||||||
func scanUser(stmt *sqlite.Stmt) *User {
|
func scanUser(stmt *sqlite.Stmt) *User {
|
||||||
user := &User{}
|
user := &User{}
|
||||||
userScanner.Scan(stmt, user)
|
userScanner.Scan(stmt, user)
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find retrieves a user by ID
|
// Query functions
|
||||||
func Find(id int) (*User, error) {
|
func Find(id int) (*User, error) {
|
||||||
var user *User
|
var user *User
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users WHERE id = ?`
|
query := `SELECT ` + userColumns() + ` FROM users WHERE id = ?`
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
user = scanUser(stmt)
|
user = scanUser(stmt)
|
||||||
return nil
|
return nil
|
||||||
}, id)
|
}, id)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to find user: %w", err)
|
return nil, fmt.Errorf("failed to find user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, fmt.Errorf("user with ID %d not found", id)
|
return nil, fmt.Errorf("user with ID %d not found", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// All retrieves all users ordered by registration date (newest first)
|
|
||||||
func All() ([]*User, error) {
|
func All() ([]*User, error) {
|
||||||
var users []*User
|
var users []*User
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users ORDER BY registered DESC, id DESC`
|
query := `SELECT ` + userColumns() + ` FROM users ORDER BY registered DESC, id DESC`
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
user := scanUser(stmt)
|
user := scanUser(stmt)
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve all users: %w", err)
|
return nil, fmt.Errorf("failed to retrieve all users: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByUsername retrieves a user by username (case-insensitive)
|
|
||||||
func ByUsername(username string) (*User, error) {
|
func ByUsername(username string) (*User, error) {
|
||||||
var user *User
|
var user *User
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
|
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
user = scanUser(stmt)
|
user = scanUser(stmt)
|
||||||
return nil
|
return nil
|
||||||
}, username)
|
}, username)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to find user by username: %w", err)
|
return nil, fmt.Errorf("failed to find user by username: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, fmt.Errorf("user with username '%s' not found", username)
|
return nil, fmt.Errorf("user with username '%s' not found", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByEmail retrieves a user by email address
|
|
||||||
func ByEmail(email string) (*User, error) {
|
func ByEmail(email string) (*User, error) {
|
||||||
var user *User
|
var user *User
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users WHERE email = ? LIMIT 1`
|
query := `SELECT ` + userColumns() + ` FROM users WHERE email = ? LIMIT 1`
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
user = scanUser(stmt)
|
user = scanUser(stmt)
|
||||||
return nil
|
return nil
|
||||||
}, email)
|
}, email)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to find user by email: %w", err)
|
return nil, fmt.Errorf("failed to find user by email: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, fmt.Errorf("user with email '%s' not found", email)
|
return nil, fmt.Errorf("user with email '%s' not found", email)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByLevel retrieves users at a specific level
|
|
||||||
func ByLevel(level int) ([]*User, error) {
|
func ByLevel(level int) ([]*User, error) {
|
||||||
var users []*User
|
var users []*User
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users WHERE level = ? ORDER BY exp DESC, id ASC`
|
query := `SELECT ` + userColumns() + ` FROM users WHERE level = ? ORDER BY exp DESC, id ASC`
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
user := scanUser(stmt)
|
user := scanUser(stmt)
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
return nil
|
return nil
|
||||||
}, level)
|
}, level)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve users by level: %w", err)
|
return nil, fmt.Errorf("failed to retrieve users by level: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Online retrieves users who have been online within the specified duration
|
|
||||||
func Online(within time.Duration) ([]*User, error) {
|
func Online(within time.Duration) ([]*User, error) {
|
||||||
var users []*User
|
var users []*User
|
||||||
cutoff := time.Now().Add(-within).Unix()
|
cutoff := time.Now().Add(-within).Unix()
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users WHERE last_online >= ? ORDER BY last_online DESC, id ASC`
|
query := `SELECT ` + userColumns() + ` FROM users WHERE last_online >= ? ORDER BY last_online DESC, id ASC`
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
||||||
user := scanUser(stmt)
|
user := scanUser(stmt)
|
||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
return nil
|
return nil
|
||||||
}, cutoff)
|
}, cutoff)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve online users: %w", err)
|
return nil, fmt.Errorf("failed to retrieve online users: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updates an existing user in the database
|
|
||||||
func (u *User) Save() error {
|
|
||||||
if u.ID == 0 {
|
|
||||||
return fmt.Errorf("cannot save user without ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `UPDATE users SET username = ?, password = ?, email = ?, verified = ?, token = ?,
|
|
||||||
registered = ?, last_online = ?, auth = ?, x = ?, y = ?, class_id = ?, currently = ?,
|
|
||||||
fighting = ?, monster_id = ?, monster_hp = ?, monster_sleep = ?, monster_immune = ?,
|
|
||||||
uber_damage = ?, uber_defense = ?, hp = ?, mp = ?, tp = ?, max_hp = ?, max_mp = ?, max_tp = ?,
|
|
||||||
level = ?, gold = ?, exp = ?, gold_bonus = ?, exp_bonus = ?, strength = ?, dexterity = ?,
|
|
||||||
attack = ?, defense = ?, weapon_id = ?, armor_id = ?, shield_id = ?, slot_1_id = ?,
|
|
||||||
slot_2_id = ?, slot_3_id = ?, weapon_name = ?, armor_name = ?, shield_name = ?,
|
|
||||||
slot_1_name = ?, slot_2_name = ?, slot_3_name = ?, drop_code = ?, spells = ?, towns = ?
|
|
||||||
WHERE id = ?`
|
|
||||||
|
|
||||||
return database.Exec(query, u.Username, u.Password, u.Email, u.Verified, u.Token,
|
|
||||||
u.Registered, u.LastOnline, u.Auth, u.X, u.Y, u.ClassID, u.Currently,
|
|
||||||
u.Fighting, u.MonsterID, u.MonsterHP, u.MonsterSleep, u.MonsterImmune,
|
|
||||||
u.UberDamage, u.UberDefense, u.HP, u.MP, u.TP, u.MaxHP, u.MaxMP, u.MaxTP,
|
|
||||||
u.Level, u.Gold, u.Exp, u.GoldBonus, u.ExpBonus, u.Strength, u.Dexterity,
|
|
||||||
u.Attack, u.Defense, u.WeaponID, u.ArmorID, u.ShieldID, u.Slot1ID,
|
|
||||||
u.Slot2ID, u.Slot3ID, u.WeaponName, u.ArmorName, u.ShieldName,
|
|
||||||
u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns, u.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert saves a new user to the database and sets the ID
|
|
||||||
func (u *User) Insert() error {
|
func (u *User) Insert() error {
|
||||||
if u.ID != 0 {
|
columns := `username, password, email, verified, token, registered, last_online, auth,
|
||||||
return fmt.Errorf("user already has ID %d, use Save() to update", u.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a transaction to ensure we can get the ID
|
|
||||||
err := database.Transaction(func(tx *database.Tx) error {
|
|
||||||
query := `INSERT INTO users (username, password, email, verified, token, registered, last_online, auth,
|
|
||||||
x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune,
|
x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune,
|
||||||
uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp,
|
uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp,
|
||||||
gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id,
|
gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id,
|
||||||
slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name,
|
slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name,
|
||||||
slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns)
|
slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns`
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
|
|
||||||
if err := tx.Exec(query, u.Username, u.Password, u.Email, u.Verified, u.Token,
|
values := []any{u.Username, u.Password, u.Email, u.Verified, u.Token,
|
||||||
u.Registered, u.LastOnline, u.Auth, u.X, u.Y, u.ClassID, u.Currently,
|
u.Registered, u.LastOnline, u.Auth, u.X, u.Y, u.ClassID, u.Currently,
|
||||||
u.Fighting, u.MonsterID, u.MonsterHP, u.MonsterSleep, u.MonsterImmune,
|
u.Fighting, u.MonsterID, u.MonsterHP, u.MonsterSleep, u.MonsterImmune,
|
||||||
u.UberDamage, u.UberDefense, u.HP, u.MP, u.TP, u.MaxHP, u.MaxMP, u.MaxTP,
|
u.UberDamage, u.UberDefense, u.HP, u.MP, u.TP, u.MaxHP, u.MaxMP, u.MaxTP,
|
||||||
u.Level, u.Gold, u.Exp, u.GoldBonus, u.ExpBonus, u.Strength, u.Dexterity,
|
u.Level, u.Gold, u.Exp, u.GoldBonus, u.ExpBonus, u.Strength, u.Dexterity,
|
||||||
u.Attack, u.Defense, u.WeaponID, u.ArmorID, u.ShieldID, u.Slot1ID,
|
u.Attack, u.Defense, u.WeaponID, u.ArmorID, u.ShieldID, u.Slot1ID,
|
||||||
u.Slot2ID, u.Slot3ID, u.WeaponName, u.ArmorName, u.ShieldName,
|
u.Slot2ID, u.Slot3ID, u.WeaponName, u.ArmorName, u.ShieldName,
|
||||||
u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns); err != nil {
|
u.Slot1Name, u.Slot2Name, u.Slot3Name, u.DropCode, u.Spells, u.Towns}
|
||||||
return fmt.Errorf("failed to insert user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the last insert ID
|
return database.Insert(u, columns, values...)
|
||||||
var id int
|
|
||||||
err := tx.Query("SELECT last_insert_rowid()", func(stmt *sqlite.Stmt) error {
|
|
||||||
id = stmt.ColumnInt(0)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get insert ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u.ID = id
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the user from the database
|
// Helper methods
|
||||||
func (u *User) Delete() error {
|
|
||||||
if u.ID == 0 {
|
|
||||||
return fmt.Errorf("cannot delete user without ID")
|
|
||||||
}
|
|
||||||
|
|
||||||
return database.Exec("DELETE FROM users WHERE id = ?", u.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisteredTime returns the registration timestamp as a time.Time
|
|
||||||
func (u *User) RegisteredTime() time.Time {
|
func (u *User) RegisteredTime() time.Time {
|
||||||
return time.Unix(u.Registered, 0)
|
return time.Unix(u.Registered, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LastOnlineTime returns the last online timestamp as a time.Time
|
|
||||||
func (u *User) LastOnlineTime() time.Time {
|
func (u *User) LastOnlineTime() time.Time {
|
||||||
return time.Unix(u.LastOnline, 0)
|
return time.Unix(u.LastOnline, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateLastOnline sets the last online timestamp to current time
|
|
||||||
func (u *User) UpdateLastOnline() {
|
func (u *User) UpdateLastOnline() {
|
||||||
u.LastOnline = time.Now().Unix()
|
u.Set("LastOnline", time.Now().Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsVerified returns true if the user's email is verified
|
|
||||||
func (u *User) IsVerified() bool {
|
func (u *User) IsVerified() bool {
|
||||||
return u.Verified == 1
|
return u.Verified == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAdmin returns true if the user has admin privileges (auth >= 4)
|
|
||||||
func (u *User) IsAdmin() bool {
|
func (u *User) IsAdmin() bool {
|
||||||
return u.Auth >= 4
|
return u.Auth >= 4
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsModerator returns true if the user has moderator privileges (auth >= 2)
|
|
||||||
func (u *User) IsModerator() bool {
|
func (u *User) IsModerator() bool {
|
||||||
return u.Auth >= 2
|
return u.Auth >= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFighting returns true if the user is currently fighting
|
|
||||||
func (u *User) IsFighting() bool {
|
func (u *User) IsFighting() bool {
|
||||||
return u.Fighting == 1
|
return u.Fighting == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsAlive returns true if the user has HP > 0
|
|
||||||
func (u *User) IsAlive() bool {
|
func (u *User) IsAlive() bool {
|
||||||
return u.HP > 0
|
return u.HP > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSpellIDs returns spell IDs as a slice of strings
|
|
||||||
func (u *User) GetSpellIDs() []string {
|
func (u *User) GetSpellIDs() []string {
|
||||||
if u.Spells == "" {
|
if u.Spells == "" {
|
||||||
return []string{}
|
return []string{}
|
||||||
@ -362,12 +289,10 @@ func (u *User) GetSpellIDs() []string {
|
|||||||
return strings.Split(u.Spells, ",")
|
return strings.Split(u.Spells, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSpellIDs sets spell IDs from a slice of strings
|
|
||||||
func (u *User) SetSpellIDs(spells []string) {
|
func (u *User) SetSpellIDs(spells []string) {
|
||||||
u.Spells = strings.Join(spells, ",")
|
u.Set("Spells", strings.Join(spells, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasSpell returns true if the user knows the specified spell ID
|
|
||||||
func (u *User) HasSpell(spellID string) bool {
|
func (u *User) HasSpell(spellID string) bool {
|
||||||
spells := u.GetSpellIDs()
|
spells := u.GetSpellIDs()
|
||||||
for _, spell := range spells {
|
for _, spell := range spells {
|
||||||
@ -378,7 +303,6 @@ func (u *User) HasSpell(spellID string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTownIDs returns town IDs as a slice of strings
|
|
||||||
func (u *User) GetTownIDs() []string {
|
func (u *User) GetTownIDs() []string {
|
||||||
if u.Towns == "" {
|
if u.Towns == "" {
|
||||||
return []string{}
|
return []string{}
|
||||||
@ -386,12 +310,10 @@ func (u *User) GetTownIDs() []string {
|
|||||||
return strings.Split(u.Towns, ",")
|
return strings.Split(u.Towns, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTownIDs sets town IDs from a slice of strings
|
|
||||||
func (u *User) SetTownIDs(towns []string) {
|
func (u *User) SetTownIDs(towns []string) {
|
||||||
u.Towns = strings.Join(towns, ",")
|
u.Set("Towns", strings.Join(towns, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasVisitedTown returns true if the user has visited the specified town ID
|
|
||||||
func (u *User) HasVisitedTown(townID string) bool {
|
func (u *User) HasVisitedTown(townID string) bool {
|
||||||
towns := u.GetTownIDs()
|
towns := u.GetTownIDs()
|
||||||
for _, town := range towns {
|
for _, town := range towns {
|
||||||
@ -402,7 +324,6 @@ func (u *User) HasVisitedTown(townID string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEquipment returns all equipped item information
|
|
||||||
func (u *User) GetEquipment() map[string]any {
|
func (u *User) GetEquipment() map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName},
|
"weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName},
|
||||||
@ -414,7 +335,6 @@ func (u *User) GetEquipment() map[string]any {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats returns combat-relevant stats
|
|
||||||
func (u *User) GetStats() map[string]int {
|
func (u *User) GetStats() map[string]int {
|
||||||
return map[string]int{
|
return map[string]int{
|
||||||
"level": u.Level,
|
"level": u.Level,
|
||||||
@ -433,59 +353,13 @@ func (u *User) GetStats() map[string]int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPosition returns the user's coordinates
|
|
||||||
func (u *User) GetPosition() (int, int) {
|
func (u *User) GetPosition() (int, int) {
|
||||||
return u.X, u.Y
|
return u.X, u.Y
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPosition sets the user's coordinates
|
|
||||||
func (u *User) SetPosition(x, y int) {
|
func (u *User) SetPosition(x, y int) {
|
||||||
u.X = x
|
u.Set("X", x)
|
||||||
u.Y = y
|
u.Set("Y", y)
|
||||||
}
|
|
||||||
|
|
||||||
// GetByUsername retrieves a user by username
|
|
||||||
func GetByUsername(username string) (*User, error) {
|
|
||||||
var user *User
|
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(username) = LOWER(?) LIMIT 1`
|
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
||||||
user = scanUser(stmt)
|
|
||||||
return nil
|
|
||||||
}, username)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
return nil, fmt.Errorf("user not found: %s", username)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByEmail retrieves a user by email
|
|
||||||
func GetByEmail(email string) (*User, error) {
|
|
||||||
var user *User
|
|
||||||
|
|
||||||
query := `SELECT ` + userColumns() + ` FROM users WHERE LOWER(email) = LOWER(?) LIMIT 1`
|
|
||||||
|
|
||||||
err := database.Query(query, func(stmt *sqlite.Stmt) error {
|
|
||||||
user = scanUser(stmt)
|
|
||||||
return nil
|
|
||||||
}, email)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("query failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == nil {
|
|
||||||
return nil, fmt.Errorf("user not found: %s", email)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap converts the user to a map for efficient template rendering
|
// ToMap converts the user to a map for efficient template rendering
|
||||||
|
@ -1,58 +1,57 @@
|
|||||||
<table width="100%">
|
<section>
|
||||||
<tr>
|
<div class="title"><img src="/assets/images/button_character.gif" alt="Character" title="Character"></div>
|
||||||
<td class="title"><img src="/assets/images/button_character.gif" alt="Character" title="Character"></td>
|
<ul class="unstyled">
|
||||||
</tr>
|
<li><b>{user.Username}</b></li>
|
||||||
<tr>
|
<li>Level: {user.Level}</li>
|
||||||
<td>
|
<li>Exp: {user.Exp}</li>
|
||||||
<b>{user.Username}</b><br>
|
<li>Gold: {user.Gold}</li>
|
||||||
Level: {user.Level}<br>
|
<li>HP: {user.HP}</li>
|
||||||
Exp: {user.Exp}<br>
|
<li>MP: {user.MP}</li>
|
||||||
Gold: {user.Gold}<br>
|
<li>TP: {user.TP}</li>
|
||||||
HP: {user.HP}<br>
|
</ul>
|
||||||
MP: {user.MP}<br>
|
|
||||||
TP: {user.TP}<br>
|
|
||||||
{statbars}<br>
|
|
||||||
<a href="javascript:opencharpopup()">Extended Stats</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
<div id="statbars">
|
||||||
|
<div class="stat">
|
||||||
|
<div id="hp" class="container"><div class="bar" style="height: {hppct}%"></div></div>
|
||||||
|
<label>HP</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat">
|
||||||
|
<div id="mp" class="container"><div class="bar" style="height: {mppct}%"></div></div>
|
||||||
|
<label>MP</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat">
|
||||||
|
<div id="tp" class="container"><div class="bar" style="height: {tppct}%"></div></div>
|
||||||
|
<label>TP</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="javascript:open_char_popup()">Extended Stats</a>
|
||||||
|
</section>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<table width="100%">
|
<section>
|
||||||
<tr>
|
<div class="title"><img src="/assets/images/button_inventory.gif" alt="Inventory" title="Inventory"></div>
|
||||||
<td class="title"><img src="/assets/images/button_inventory.gif" alt="Inventory" title="Inventory"></td>
|
<div>
|
||||||
</tr>
|
<img src="/assets/images/icon_weapon.gif" alt="Weapon" title="Weapon">
|
||||||
<tr>
|
{weaponname}
|
||||||
<td>
|
</div>
|
||||||
<table width="100%">
|
<div>
|
||||||
<tr>
|
<img src="/assets/images/icon_armor.gif" alt="Armor" title="Armor">
|
||||||
<td><img src="/assets/images/icon_weapon.gif" alt="Weapon" title="Weapon"></td>
|
{armorname}
|
||||||
<td width="100%">{weaponname}</td>
|
</div>
|
||||||
</tr>
|
<div>
|
||||||
<tr>
|
<img src="/assets/images/icon_shield.gif" alt="Shield" title="Shield">
|
||||||
<td><img src="/assets/images/icon_armor.gif" alt="Armor" title="Armor"></td>
|
{shieldname}
|
||||||
<td width="100%">{armorname}</td>
|
</div>
|
||||||
</tr>
|
{slot1name}
|
||||||
<tr>
|
{slot2name}
|
||||||
<td><img src="/assets/images/icon_shield.gif" alt="Shield" title="Shield"></td>
|
|
||||||
<td width="100%">{shieldname}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
{slot1name}<br>
|
|
||||||
{slot2name}<br>
|
|
||||||
{slot3name}
|
{slot3name}
|
||||||
</td>
|
</section>
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<br>
|
<section>
|
||||||
|
<div class="title"><img src="/assets/images/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></div>
|
||||||
<table width="100%">
|
{magiclist}
|
||||||
<tr>
|
</section>
|
||||||
<td class="title"><img src="/assets/images/button_fastspells.gif" alt="Fast Spells" title="Fast Spells"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{magiclist}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
Loading…
x
Reference in New Issue
Block a user