big item rework

This commit is contained in:
Sky Johnson 2025-08-25 16:35:55 -05:00
parent c226f6200f
commit bbd515bc57
12 changed files with 618 additions and 204 deletions

Binary file not shown.

2
go.mod
View File

@ -3,7 +3,7 @@ module dk
go 1.25.0 go 1.25.0
require ( require (
git.sharkk.net/Sharkk/Sashimi v1.1.1 git.sharkk.net/Sharkk/Sashimi v1.1.3
git.sharkk.net/Sharkk/Sushi v1.2.0 git.sharkk.net/Sharkk/Sushi v1.2.0
github.com/valyala/fasthttp v1.65.0 github.com/valyala/fasthttp v1.65.0
) )

4
go.sum
View File

@ -1,5 +1,5 @@
git.sharkk.net/Sharkk/Sashimi v1.1.1 h1:ST7YQjnQq3ydAKy/Kerx89cx+b/mXDOq7W/ll1aX1jA= git.sharkk.net/Sharkk/Sashimi v1.1.3 h1:fY63Zn//A1EffFkoKjCQseRmLFNRibNDZYPUur5SF1s=
git.sharkk.net/Sharkk/Sashimi v1.1.1/go.mod h1:wTMnO6jo34LIjpDJ0qToq14RbwP6Uf4HtdWDmqxrdAM= git.sharkk.net/Sharkk/Sashimi v1.1.3/go.mod h1:wTMnO6jo34LIjpDJ0qToq14RbwP6Uf4HtdWDmqxrdAM=
git.sharkk.net/Sharkk/Sushi v1.2.0 h1:RwOCZmgaOqtkmuK2Z7/esdLbhSXJZphsOsWEHni4Sss= git.sharkk.net/Sharkk/Sushi v1.2.0 h1:RwOCZmgaOqtkmuK2Z7/esdLbhSXJZphsOsWEHni4Sss=
git.sharkk.net/Sharkk/Sushi v1.2.0/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o= git.sharkk.net/Sharkk/Sushi v1.2.0/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=

View File

@ -51,7 +51,7 @@ func Move(user *users.User, dir Direction) (string, int, int, error) {
} }
if !control.IsWithinWorldBounds(newX, newY) { if !control.IsWithinWorldBounds(newX, newY) {
return user.Currently, user.X, user.Y, fmt.Errorf("You've hit the edge of the world.") return user.Currently, user.X, user.Y, fmt.Errorf("you've hit the edge of the world")
} }
if town, _ := towns.ByCoords(newX, newY); town != nil { if town, _ := towns.ByCoords(newX, newY); town != nil {

View File

@ -1,57 +1,359 @@
package actions package actions
import ( import (
"fmt"
"dk/internal/models/items" "dk/internal/models/items"
"dk/internal/models/users" "dk/internal/models/users"
) )
// UserEquipItem calculates equipment updates for a user equipping an item. // UserEquipItem calculates equipment updates for a user equipping an item.
// Returns map of database field updates without modifying the user struct. // Returns map of database field updates without modifying the user struct.
func UserEquipItem(user *users.User, item *items.Item) map[string]any { func UserEquipItem(user *users.User, item *items.Item) (map[string]any, error) {
if !item.IsEquippable() && !item.IsSlottable() {
return nil, fmt.Errorf("item %s is not equippable", item.Name)
}
updates := make(map[string]any) updates := make(map[string]any)
// Calculate stat changes // Calculate stat changes by removing old item and adding new item
newAttack := user.Attack newStats := calculateNewStats(user, item)
newDefense := user.Defense
// Remove old item stats if slot occupied // Update equipment slots and stats based on item type
switch item.Type { switch item.Type {
case items.TypeWeapon: case items.TypeWeapon:
if user.WeaponID != 0 {
if oldItem, err := items.Find(user.WeaponID); err == nil {
newAttack -= oldItem.Att
}
}
// Add new item
newAttack += item.Att
updates["weapon_id"] = item.ID updates["weapon_id"] = item.ID
updates["weapon_name"] = item.Name updates["weapon_name"] = item.Name
case items.TypeArmor: case items.TypeArmor:
if user.ArmorID != 0 {
if oldItem, err := items.Find(user.ArmorID); err == nil {
newDefense -= oldItem.Att
}
}
// Add new item
newDefense += item.Att
updates["armor_id"] = item.ID updates["armor_id"] = item.ID
updates["armor_name"] = item.Name updates["armor_name"] = item.Name
case items.TypeShield: case items.TypeShield:
if user.ShieldID != 0 {
if oldItem, err := items.Find(user.ShieldID); err == nil {
newDefense -= oldItem.Att
}
}
// Add new item
newDefense += item.Att
updates["shield_id"] = item.ID updates["shield_id"] = item.ID
updates["shield_name"] = item.Name updates["shield_name"] = item.Name
case items.TypeAccessory:
// Find first available slot or replace slot 1
if user.Slot1ID == 0 {
updates["slot_1_id"] = item.ID
updates["slot_1_name"] = item.Name
} else if user.Slot2ID == 0 {
updates["slot_2_id"] = item.ID
updates["slot_2_name"] = item.Name
} else if user.Slot3ID == 0 {
updates["slot_3_id"] = item.ID
updates["slot_3_name"] = item.Name
} else {
// All slots full, replace slot 1
updates["slot_1_id"] = item.ID
updates["slot_1_name"] = item.Name
}
} }
updates["attack"] = newAttack // Apply stat changes
updates["defense"] = newDefense updates["attack"] = newStats.Attack
updates["defense"] = newStats.Defense
updates["strength"] = newStats.Strength
updates["dexterity"] = newStats.Dexterity
updates["max_hp"] = newStats.MaxHP
updates["max_mp"] = newStats.MaxMP
updates["exp_bonus"] = newStats.ExpBonus
updates["gold_bonus"] = newStats.GoldBonus
return updates return updates, nil
}
// UserEquipAccessoryToSlot equips an accessory to a specific slot (1-3)
func UserEquipAccessoryToSlot(user *users.User, item *items.Item, slot int) (map[string]any, error) {
if !item.IsSlottable() {
return nil, fmt.Errorf("item %s is not slottable", item.Name)
}
if slot < 1 || slot > 3 {
return nil, fmt.Errorf("invalid slot number: %d", slot)
}
updates := make(map[string]any)
newStats := calculateNewStatsForSlot(user, item, slot)
// Update the specific slot
switch slot {
case 1:
updates["slot_1_id"] = item.ID
updates["slot_1_name"] = item.Name
case 2:
updates["slot_2_id"] = item.ID
updates["slot_2_name"] = item.Name
case 3:
updates["slot_3_id"] = item.ID
updates["slot_3_name"] = item.Name
}
// Apply stat changes
updates["attack"] = newStats.Attack
updates["defense"] = newStats.Defense
updates["strength"] = newStats.Strength
updates["dexterity"] = newStats.Dexterity
updates["max_hp"] = newStats.MaxHP
updates["max_mp"] = newStats.MaxMP
updates["exp_bonus"] = newStats.ExpBonus
updates["gold_bonus"] = newStats.GoldBonus
return updates, nil
}
// UserUnequipItem removes an equipped item and recalculates stats
func UserUnequipItem(user *users.User, itemType int) (map[string]any, error) {
updates := make(map[string]any)
switch itemType {
case items.TypeWeapon:
if user.WeaponID == 0 {
return nil, fmt.Errorf("no weapon equipped")
}
updates["weapon_id"] = 0
updates["weapon_name"] = ""
case items.TypeArmor:
if user.ArmorID == 0 {
return nil, fmt.Errorf("no armor equipped")
}
updates["armor_id"] = 0
updates["armor_name"] = ""
case items.TypeShield:
if user.ShieldID == 0 {
return nil, fmt.Errorf("no shield equipped")
}
updates["shield_id"] = 0
updates["shield_name"] = ""
default:
return nil, fmt.Errorf("invalid item type for unequip: %d", itemType)
}
// Recalculate all stats after unequipping
newStats := recalculateAllStats(user, itemType, 0)
updates["attack"] = newStats.Attack
updates["defense"] = newStats.Defense
updates["strength"] = newStats.Strength
updates["dexterity"] = newStats.Dexterity
updates["max_hp"] = newStats.MaxHP
updates["max_mp"] = newStats.MaxMP
updates["exp_bonus"] = newStats.ExpBonus
updates["gold_bonus"] = newStats.GoldBonus
return updates, nil
}
// UserUnequipAccessory removes an accessory from a specific slot
func UserUnequipAccessory(user *users.User, slot int) (map[string]any, error) {
if slot < 1 || slot > 3 {
return nil, fmt.Errorf("invalid slot number: %d", slot)
}
updates := make(map[string]any)
// Check if slot has an item
switch slot {
case 1:
if user.Slot1ID == 0 {
return nil, fmt.Errorf("slot 1 is empty")
}
updates["slot_1_id"] = 0
updates["slot_1_name"] = ""
case 2:
if user.Slot2ID == 0 {
return nil, fmt.Errorf("slot 2 is empty")
}
updates["slot_2_id"] = 0
updates["slot_2_name"] = ""
case 3:
if user.Slot3ID == 0 {
return nil, fmt.Errorf("slot 3 is empty")
}
updates["slot_3_id"] = 0
updates["slot_3_name"] = ""
}
// Recalculate stats after removing accessory
newStats := recalculateAllStats(user, items.TypeAccessory, slot)
updates["attack"] = newStats.Attack
updates["defense"] = newStats.Defense
updates["strength"] = newStats.Strength
updates["dexterity"] = newStats.Dexterity
updates["max_hp"] = newStats.MaxHP
updates["max_mp"] = newStats.MaxMP
updates["exp_bonus"] = newStats.ExpBonus
updates["gold_bonus"] = newStats.GoldBonus
return updates, nil
}
// Stats represents calculated user stats
type Stats struct {
Attack int
Defense int
Strength int
Dexterity int
MaxHP int
MaxMP int
ExpBonus int
GoldBonus int
}
// calculateNewStats calculates new stats when equipping an item
func calculateNewStats(user *users.User, newItem *items.Item) Stats {
stats := Stats{
Attack: user.Attack,
Defense: user.Defense,
Strength: user.Strength,
Dexterity: user.Dexterity,
MaxHP: user.MaxHP,
MaxMP: user.MaxMP,
ExpBonus: user.ExpBonus,
GoldBonus: user.GoldBonus,
}
// Remove old item stats if slot occupied
switch newItem.Type {
case items.TypeWeapon:
if user.WeaponID != 0 {
if oldItem, err := items.Find(user.WeaponID); err == nil {
removeItemStats(&stats, oldItem)
}
}
case items.TypeArmor:
if user.ArmorID != 0 {
if oldItem, err := items.Find(user.ArmorID); err == nil {
removeItemStats(&stats, oldItem)
}
}
case items.TypeShield:
if user.ShieldID != 0 {
if oldItem, err := items.Find(user.ShieldID); err == nil {
removeItemStats(&stats, oldItem)
}
}
}
// Add new item stats
addItemStats(&stats, newItem)
return stats
}
// calculateNewStatsForSlot calculates stats when equipping to a specific accessory slot
func calculateNewStatsForSlot(user *users.User, newItem *items.Item, slot int) Stats {
stats := Stats{
Attack: user.Attack,
Defense: user.Defense,
Strength: user.Strength,
Dexterity: user.Dexterity,
MaxHP: user.MaxHP,
MaxMP: user.MaxMP,
ExpBonus: user.ExpBonus,
GoldBonus: user.GoldBonus,
}
// Remove old accessory stats from the specified slot
var oldItemID int
switch slot {
case 1:
oldItemID = user.Slot1ID
case 2:
oldItemID = user.Slot2ID
case 3:
oldItemID = user.Slot3ID
}
if oldItemID != 0 {
if oldItem, err := items.Find(oldItemID); err == nil {
removeItemStats(&stats, oldItem)
}
}
// Add new item stats
addItemStats(&stats, newItem)
return stats
}
// recalculateAllStats recalculates all stats from scratch, excluding specified item
func recalculateAllStats(user *users.User, excludeType, excludeSlot int) Stats {
// Start with base stats (these would come from class/level progression)
// For now, using current stats minus all equipment bonuses
stats := Stats{
Attack: 0, // Base attack from strength/level
Defense: 0, // Base defense from dexterity/level
Strength: user.Strength,
Dexterity: user.Dexterity,
MaxHP: user.MaxHP,
MaxMP: user.MaxMP,
ExpBonus: 0, // Base exp bonus
GoldBonus: 0, // Base gold bonus
}
// Add weapon stats (unless being removed)
if user.WeaponID != 0 && excludeType != items.TypeWeapon {
if item, err := items.Find(user.WeaponID); err == nil {
addItemStats(&stats, item)
}
}
// Add armor stats (unless being removed)
if user.ArmorID != 0 && excludeType != items.TypeArmor {
if item, err := items.Find(user.ArmorID); err == nil {
addItemStats(&stats, item)
}
}
// Add shield stats (unless being removed)
if user.ShieldID != 0 && excludeType != items.TypeShield {
if item, err := items.Find(user.ShieldID); err == nil {
addItemStats(&stats, item)
}
}
// Add accessory stats (unless specific slot being removed)
if user.Slot1ID != 0 && !(excludeType == items.TypeAccessory && excludeSlot == 1) {
if item, err := items.Find(user.Slot1ID); err == nil {
addItemStats(&stats, item)
}
}
if user.Slot2ID != 0 && !(excludeType == items.TypeAccessory && excludeSlot == 2) {
if item, err := items.Find(user.Slot2ID); err == nil {
addItemStats(&stats, item)
}
}
if user.Slot3ID != 0 && !(excludeType == items.TypeAccessory && excludeSlot == 3) {
if item, err := items.Find(user.Slot3ID); err == nil {
addItemStats(&stats, item)
}
}
return stats
}
// addItemStats adds an item's stats to the current stats
func addItemStats(stats *Stats, item *items.Item) {
stats.Attack += item.Attack
stats.Defense += item.Defense
stats.Strength += item.Strength
stats.Dexterity += item.Dexterity
stats.MaxHP += item.MaxHP
stats.MaxMP += item.MaxMP
stats.ExpBonus += item.ExpBonus
stats.GoldBonus += item.GoldBonus
}
// removeItemStats removes an item's stats from the current stats
func removeItemStats(stats *Stats, item *items.Item) {
stats.Attack -= item.Attack
stats.Defense -= item.Defense
stats.Strength -= item.Strength
stats.Dexterity -= item.Dexterity
stats.MaxHP -= item.MaxHP
stats.MaxMP -= item.MaxMP
stats.ExpBonus -= item.ExpBonus
stats.GoldBonus -= item.GoldBonus
} }

View File

@ -1,101 +0,0 @@
package drops
import (
"fmt"
"dk/internal/database"
)
// Drop represents a drop item in the game
type Drop struct {
ID int
Name string
Level int
Type int
Att string
}
// DropType constants for drop types
const (
TypeConsumable = 1
)
// New creates a new Drop with sensible defaults
func New() *Drop {
return &Drop{
Name: "",
Level: 1,
Type: TypeConsumable,
Att: "",
}
}
// Validate checks if drop has valid values
func (d *Drop) Validate() error {
if d.Name == "" {
return fmt.Errorf("drop name cannot be empty")
}
if d.Level < 1 {
return fmt.Errorf("drop Level must be at least 1")
}
if d.Type < TypeConsumable {
return fmt.Errorf("invalid drop type: %d", d.Type)
}
return nil
}
// CRUD operations
func (d *Drop) Delete() error {
return database.Exec("DELETE FROM drops WHERE id = %d", d.ID)
}
func (d *Drop) Insert() error {
id, err := database.Insert("drops", d, "ID")
if err != nil {
return err
}
d.ID = int(id)
return nil
}
// Query functions
func Find(id int) (*Drop, error) {
var drop Drop
err := database.Get(&drop, "SELECT * FROM drops WHERE id = %d", id)
if err != nil {
return nil, fmt.Errorf("drop with ID %d not found", id)
}
return &drop, nil
}
func All() ([]*Drop, error) {
var drops []*Drop
err := database.Select(&drops, "SELECT * FROM drops ORDER BY id ASC")
return drops, err
}
func ByLevel(minLevel int) ([]*Drop, error) {
var drops []*Drop
err := database.Select(&drops, "SELECT * FROM drops WHERE level <= %d ORDER BY id ASC", minLevel)
return drops, err
}
func ByType(dropType int) ([]*Drop, error) {
var drops []*Drop
err := database.Select(&drops, "SELECT * FROM drops WHERE type = %d ORDER BY id ASC", dropType)
return drops, err
}
// Helper methods
func (d *Drop) IsConsumable() bool {
return d.Type == TypeConsumable
}
func (d *Drop) TypeName() string {
switch d.Type {
case TypeConsumable:
return "Consumable"
default:
return "Unknown"
}
}

View File

@ -8,29 +8,46 @@ import (
// Item represents an item in the game // Item represents an item in the game
type Item struct { type Item struct {
ID int ID int `db:"id"`
Type int Type int `db:"type"`
Name string Name string `db:"name"`
Value int Lore string `db:"lore"`
Att int Value int `db:"value"`
Special string Attack int `db:"attack"`
Defense int `db:"defense"`
Strength int `db:"strength"`
Dexterity int `db:"dexterity"`
MaxHP int `db:"max_hp"`
MaxMP int `db:"max_mp"`
ExpBonus int `db:"exp_bonus"`
GoldBonus int `db:"gold_bonus"`
Special string `db:"special"`
} }
// ItemType constants for item types // ItemType constants for item types
const ( const (
TypeWeapon = 1 TypeWeapon = 1
TypeArmor = 2 TypeArmor = 2
TypeShield = 3 TypeShield = 3
TypeAccessory = 4
) )
// New creates a new Item with sensible defaults // New creates a new Item with sensible defaults
func New() *Item { func New() *Item {
return &Item{ return &Item{
Type: TypeWeapon, Type: TypeWeapon,
Name: "", Name: "",
Value: 0, Lore: "",
Att: 0, Value: 0,
Special: "", Attack: 0,
Defense: 0,
Strength: 0,
Dexterity: 0,
MaxHP: 0,
MaxMP: 0,
ExpBonus: 0,
GoldBonus: 0,
Special: "",
} }
} }
@ -39,14 +56,11 @@ func (i *Item) Validate() error {
if i.Name == "" { if i.Name == "" {
return fmt.Errorf("item name cannot be empty") return fmt.Errorf("item name cannot be empty")
} }
if i.Type < TypeWeapon || i.Type > TypeShield { if i.Type < TypeWeapon || i.Type > TypeAccessory {
return fmt.Errorf("invalid item type: %d", i.Type) return fmt.Errorf("invalid item type: %d", i.Type)
} }
if i.Value < 0 { if i.Value < 0 {
return fmt.Errorf("item Value cannot be negative") return fmt.Errorf("item value cannot be negative")
}
if i.Att < 0 {
return fmt.Errorf("item Att cannot be negative")
} }
return nil return nil
} }
@ -57,7 +71,7 @@ func (i *Item) Delete() error {
} }
func (i *Item) Insert() error { func (i *Item) Insert() error {
id, err := database.Insert("items", i, "ID") id, err := database.Insert("items", i, "id")
if err != nil { if err != nil {
return err return err
} }
@ -65,6 +79,25 @@ func (i *Item) Insert() error {
return nil return nil
} }
func (i *Item) Update() error {
fields := map[string]any{
"type": i.Type,
"name": i.Name,
"lore": i.Lore,
"value": i.Value,
"attack": i.Attack,
"defense": i.Defense,
"strength": i.Strength,
"dexterity": i.Dexterity,
"max_hp": i.MaxHP,
"max_mp": i.MaxMP,
"exp_bonus": i.ExpBonus,
"gold_bonus": i.GoldBonus,
"special": i.Special,
}
return database.Update("items", fields, "id", i.ID)
}
// Query functions // Query functions
func Find(id int) (*Item, error) { func Find(id int) (*Item, error) {
var item Item var item Item
@ -77,13 +110,19 @@ func Find(id int) (*Item, error) {
func All() ([]*Item, error) { func All() ([]*Item, error) {
var items []*Item var items []*Item
err := database.Select(&items, "SELECT * FROM items ORDER BY id ASC") err := database.Select(&items, "SELECT * FROM items ORDER BY type ASC, value ASC, id ASC")
return items, err return items, err
} }
func ByType(itemType int) ([]*Item, error) { func ByType(itemType int) ([]*Item, error) {
var items []*Item var items []*Item
err := database.Select(&items, "SELECT * FROM items WHERE type = %d ORDER BY id ASC", itemType) err := database.Select(&items, "SELECT * FROM items WHERE type = %d ORDER BY value ASC, id ASC", itemType)
return items, err
}
func ByValueRange(minValue, maxValue int) ([]*Item, error) {
var items []*Item
err := database.Select(&items, "SELECT * FROM items WHERE value >= %d AND value <= %d ORDER BY value ASC, id ASC", minValue, maxValue)
return items, err return items, err
} }
@ -100,6 +139,10 @@ func (i *Item) IsShield() bool {
return i.Type == TypeShield return i.Type == TypeShield
} }
func (i *Item) IsAccessory() bool {
return i.Type == TypeAccessory
}
func (i *Item) TypeName() string { func (i *Item) TypeName() string {
switch i.Type { switch i.Type {
case TypeWeapon: case TypeWeapon:
@ -108,6 +151,8 @@ func (i *Item) TypeName() string {
return "Armor" return "Armor"
case TypeShield: case TypeShield:
return "Shield" return "Shield"
case TypeAccessory:
return "Accessory"
default: default:
return "Unknown" return "Unknown"
} }
@ -118,5 +163,113 @@ func (i *Item) HasSpecial() bool {
} }
func (i *Item) IsEquippable() bool { func (i *Item) IsEquippable() bool {
return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield return i.Type >= TypeWeapon && i.Type <= TypeShield
}
func (i *Item) IsSlottable() bool {
return i.Type == TypeAccessory
}
func (i *Item) HasLore() bool {
return i.Lore != ""
}
// Stat bonus methods
func (i *Item) HasAttackBonus() bool {
return i.Attack != 0
}
func (i *Item) HasDefenseBonus() bool {
return i.Defense != 0
}
func (i *Item) HasStrengthBonus() bool {
return i.Strength != 0
}
func (i *Item) HasDexterityBonus() bool {
return i.Dexterity != 0
}
func (i *Item) HasHPBonus() bool {
return i.MaxHP != 0
}
func (i *Item) HasMPBonus() bool {
return i.MaxMP != 0
}
func (i *Item) HasExpBonus() bool {
return i.ExpBonus != 0
}
func (i *Item) HasGoldBonus() bool {
return i.GoldBonus != 0
}
// Returns true if the item provides any stat bonuses
func (i *Item) HasStatBonuses() bool {
return i.Attack != 0 || i.Defense != 0 || i.Strength != 0 ||
i.Dexterity != 0 || i.MaxHP != 0 || i.MaxMP != 0 ||
i.ExpBonus != 0 || i.GoldBonus != 0
}
func (i *Item) GetStatBonuses() map[string]int {
bonuses := make(map[string]int)
if i.Attack != 0 {
bonuses["Attack"] = i.Attack
}
if i.Defense != 0 {
bonuses["Defense"] = i.Defense
}
if i.Strength != 0 {
bonuses["Strength"] = i.Strength
}
if i.Dexterity != 0 {
bonuses["Dexterity"] = i.Dexterity
}
if i.MaxHP != 0 {
bonuses["Max HP"] = i.MaxHP
}
if i.MaxMP != 0 {
bonuses["Max MP"] = i.MaxMP
}
if i.ExpBonus != 0 {
bonuses["Exp Bonus"] = i.ExpBonus
}
if i.GoldBonus != 0 {
bonuses["Gold Bonus"] = i.GoldBonus
}
return bonuses
}
// GetPrimaryStatBonus returns the main stat bonus for the item type
func (i *Item) GetPrimaryStatBonus() int {
switch i.Type {
case TypeWeapon:
return i.Attack
case TypeArmor, TypeShield:
return i.Defense
case TypeAccessory:
// For accessories, return the highest stat bonus
max := 0
stats := []int{i.Attack, i.Defense, i.Strength, i.Dexterity, i.MaxHP, i.MaxMP}
for _, stat := range stats {
if abs(stat) > abs(max) {
max = stat
}
}
return max
default:
return 0
}
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
} }

View File

@ -176,7 +176,12 @@ func buyItem(ctx sushi.Ctx) {
} }
// Get equipment updates from actions // Get equipment updates from actions
equipUpdates := actions.UserEquipItem(user, item) equipUpdates, err := actions.UserEquipItem(user, item)
if err != nil {
sess.SetFlash("error", "Cannot equip item: "+err.Error())
ctx.Redirect("/town/shop")
return
}
err = database.Transaction(func() error { err = database.Transaction(func() error {
// Start with gold deduction // Start with gold deduction

View File

@ -63,50 +63,6 @@ CREATE TABLE forum (
`content` TEXT NOT NULL `content` TEXT NOT NULL
); );
DROP TABLE IF EXISTS items;
CREATE TABLE items (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`type` INTEGER NOT NULL DEFAULT 1,
`name` TEXT NOT NULL,
`value` INTEGER NOT NULL DEFAULT 0,
`att` INTEGER NOT NULL DEFAULT 0,
`special` TEXT NOT NULL DEFAULT ''
);
INSERT INTO items VALUES
(1, 1, 'Stick', 10, 2, ''),
(2, 1, 'Branch', 30, 4, ''),
(3, 1, 'Club', 40, 5, ''),
(4, 1, 'Dagger', 90, 8, ''),
(5, 1, 'Hatchet', 150, 12, ''),
(6, 1, 'Axe', 200, 16, ''),
(7, 1, 'Brand', 300, 25, ''),
(8, 1, 'Poleaxe', 500, 35, ''),
(9, 1, 'Broadsword', 800, 45, ''),
(10, 1, 'Battle Axe', 1200, 50, ''),
(11, 1, 'Claymore', 2000, 60, ''),
(12, 1, 'Dark Axe', 3000, 100, 'expbonus,-5'),
(13, 1, 'Dark Sword', 4500, 125, 'expbonus,-10'),
(14, 1, 'Bright Sword', 6000, 100, 'expbonus,10'),
(15, 1, 'Magic Sword', 10000, 150, 'maxmp,50'),
(16, 1, 'Destiny Blade', 50000, 250, 'strength,50'),
(17, 2, 'Skivvies', 25, 2, 'goldbonus,10'),
(18, 2, 'Clothes', 50, 5, ''),
(19, 2, 'Leather Armor', 75, 10, ''),
(20, 2, 'Hard Leather Armor', 150, 25, ''),
(21, 2, 'Chain Mail', 300, 30, ''),
(22, 2, 'Bronze Plate', 900, 50, ''),
(23, 2, 'Iron Plate', 2000, 100, ''),
(24, 2, 'Magic Armor', 4000, 125, 'maxmp,50'),
(25, 2, 'Dark Armor', 5000, 150, 'expbonus,-10'),
(26, 2, 'Bright Armor', 10000, 175, 'expbonus,10'),
(27, 2, 'Destiny Raiment', 50000, 200, 'dexterity,50'),
(28, 3, 'Reed Shield', 50, 2, ''),
(29, 3, 'Buckler', 100, 4, ''),
(30, 3, 'Small Shield', 500, 10, ''),
(31, 3, 'Large Shield', 2500, 30, ''),
(32, 3, 'Silver Shield', 10000, 60, ''),
(33, 3, 'Destiny Aegis', 25000, 100, 'maxhp,50');
DROP TABLE IF EXISTS classes; DROP TABLE IF EXISTS classes;
CREATE TABLE classes ( CREATE TABLE classes (
'id' INTEGER PRIMARY KEY AUTOINCREMENT, 'id' INTEGER PRIMARY KEY AUTOINCREMENT,

View File

@ -0,0 +1,88 @@
-- Migration 2: rework items system
-- Created: 2025-08-25 10:50:50
DROP TABLE IF EXISTS drops;
DROP TABLE IF EXISTS items;
CREATE TABLE items (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`type` INTEGER NOT NULL DEFAULT 1,
`name` TEXT NOT NULL,
`lore` TEXT DEFAULT '',
`value` INTEGER NOT NULL DEFAULT 0,
`attack` INTEGER NOT NULL DEFAULT 0,
`defense` INTEGER NOT NULL DEFAULT 0,
`strength` INTEGER NOT NULL DEFAULT 0,
`dexterity` INTEGER NOT NULL DEFAULT 0,
`max_hp` INTEGER NOT NULL DEFAULT 0,
`max_mp` INTEGER NOT NULL DEFAULT 0,
`exp_bonus` INTEGER NOT NULL DEFAULT 0,
`gold_bonus` INTEGER NOT NULL DEFAULT 0,
`special` TEXT NOT NULL DEFAULT ''
);
INSERT INTO items VALUES
(1, 1, 'Stick', 'Just a stick.', 10, 2, 0, 0, 0, 0, 0, 0, 0, ''),
(2, 1, 'Branch', 'Slightly larger stick.', 30, 4, 0, 0, 0, 0, 0, 0, 0, ''),
(3, 1, 'Club', 'Much larger stick.', 40, 5, 0, 0, 0, 0, 0, 0, 0, ''),
(4, 1, 'Dagger', 'Well-crafted knife.', 90, 8, 0, 0, 0, 0, 0, 0, 0, ''),
(5, 1, 'Hatchet', 'A smaller, sharpened axe.', 150, 12, 0, 0, 0, 0, 0, 0, 0, ''),
(6, 1, 'Axe', 'Full-sized, razor-sharp axe.', 200, 16, 0, 0, 0, 0, 0, 0, 0, ''),
(7, 1, 'Brand', 'An oddly-shaped, large knife.', 300, 25, 0, 0, 0, 0, 0, 0, 0, ''),
(8, 1, 'Poleaxe', 'A weighty axe on a long stick.', 500, 35, 0, 0, 0, 0, 0, 0, 0, ''),
(9, 1, 'Broadsword', 'Thick and heavy sword.', 800, 45, 0, 0, 0, 0, 0, 0, 0, ''),
(10, 1, 'Battle Axe', 'A large axe with spikes on the rear for catching armor.', 1200, 50, 0, 0, 0, 0, 0, 0, 0, ''),
(11, 1, 'Claymore', 'A very long sword made for cleaving through armor.', 2000, 60, 0, 0, 0, 0, 0, 0, 0, ''),
(12, 1, 'Dark Axe', 'Cursed axe with a dark aura - seems to be sharper than most, but strangely draining.', 3000, 100, 0, 0, 0, 0, 0, -5, 0, ''),
(13, 1, 'Dark Sword', 'Exceptionally sharp sword. The blade seems to shine with malice - you can feel it absorbing some of your will.', 4500, 125, 0, 0, 0, 0, 0, -10, 0, ''),
(14, 1, 'Bright Sword', 'The golden hilt and silver blade make this sword shine in the sun. Defeating enemies with it makes you feel fulfilled.', 6000, 100, 0, 0, 0, 0, 0, 10, 0, ''),
(15, 1, 'Magic Sword', 'One of Gilead''s mass-produced mage blades. Crafted with skill, enchanted by apprentices.', 10000, 150, 0, 0, 0, 0, 50, 0, 0, ''),
(16, 1, 'Destiny Blade', 'The sword that cleaves through demons like butter; a lost relic from humanity''s champion. Its immense strength is palpable.', 50000, 250, 0, 50, 0, 0, 0, 5, 5, ''),
(17, 2, 'Skivvies', 'Underwear! Plain and simple.', 25, 0, 2, 0, 0, 0, 0, 0, 10, ''),
(18, 2, 'Clothes', 'Standard working attire.', 50, 0, 5, 0, 0, 0, 0, 0, 0, ''),
(19, 2, 'Leather Armor', 'Basic protection provided by supple hide.', 75, 0, 10, 0, 0, 0, 0, 0, 0, ''),
(20, 2, 'Studded Armor', 'Leather armor enhanced with small metal buckles.', 150, 0, 25, 0, 0, 0, 0, 0, 0, ''),
(21, 2, 'Chain Mail', 'Iron links, pieced together to form a dense shirt.', 300, 0, 30, 0, 0, 0, 0, 0, 0, ''),
(22, 2, 'Bronze Plate', 'Pieces of bronze formed to your vital areas, secured with leather straps.', 900, 0, 50, 0, 0, 0, 0, 0, 0, ''),
(23, 2, 'Iron Plate', 'Stronger and slightly lighter than bronze plates.', 2000, 0, 100, 0, 0, 0, 0, 0, 0, ''),
(24, 2, 'Magic Armor', 'Steel armor crafted by the skilled artisans of Gilead, enchanted by apprentices.', 4000, 0, 125, 0, 0, 0, 50, 0, 0, ''),
(25, 2, 'Dark Armor', 'Armor that feels unnaturally heavy, although it also feels unnaturaly safe.', 5000, 0, 150, 0, 0, 0, 0, -10, 0, ''),
(26, 2, 'Bright Armor', 'These plates reflect the suns rays. You seem to understand your enemies attacks easier in it.', 10000, 0, 120, 0, 0, 0, 0, 10, 0, ''),
(27, 2, 'Destiny Raiment', 'A dazzling array of fabric and engineering layered over steel alloy. Despite the wars it has seen, it bears no scars.', 50000, 0, 200, 0, 50, 0, 0, 5, 5, ''),
(28, 3, 'Reed Shield', 'Stiff river reeds. Dried, cured, strapped together with twine.', 50, 0, 2, 0, 0, 0, 0, 0, 0, ''),
(29, 3, 'Buckler', 'Planks of wood cleaned up and formed into a small, round shield.', 100, 0, 4, 0, 0, 0, 0, 0, 0, ''),
(30, 3, 'Small Shield', 'Cedar is treated to withstand impacts, then bonded together with rivets.', 500, 0, 10, 0, 0, 0, 0, 0, 0, ''),
(31, 3, 'Tower Shield', 'A very heavy, tall shield built to withstand might blows.', 2500, 0, 30, 0, 0, 0, 0, 0, 0, ''),
(32, 3, 'Silver Shield', 'Surprisingly sturdy, this gleaming shield lets attacks bounce right off.', 10000, 0, 60, 0, 0, 0, 0, 0, 0, ''),
(33, 3, 'Destiny Aegis', 'An exquisite piece of forging, it is surprisingly light. Impacts seem to be absorbed into nothing.', 25000, 0, 100, 0, 0, 50, 0, 5, 5, ''),
(34, 4, 'Life Pebble', 'Shiny green rock. Seems to put a spring in your step.', 0, 0, 0, 0, 0, 10, 0, 0, 0, ''),
(35, 4, 'Life Stone', 'Glistening green rock. You feel energized with it.', 0, 0, 0, 0, 0, 25, 0, 0, 0, ''),
(36, 4, 'Life Rock', 'Polished green rock. You feel as if you''ll live longer with it.', 0, 0, 0, 0, 0, 50, 0, 0, 0, ''),
(37, 4, 'Magic Pebble', 'Shiny blue rock. You feel at ease with it.', 0, 0, 0, 0, 0, 0, 10, 0, 0, ''),
(38, 4, 'Magic Stone', 'Glistening blue rock. It feels like it gives you clarity.', 0, 0, 0, 0, 0, 0, 25, 0, 0, ''),
(39, 4, 'Magic Rock', 'Polished blue rock. It seems to hum in your mind.', 0, 0, 0, 0, 0, 0, 50, 0, 0, ''),
(40, 4, 'Dragon''s Scale', 'A scale from a young dragon.', 0, 0, 25, 0, 0, 0, 0, 0, 0, ''),
(41, 4, 'Dragon''s Plate', 'A piece of the ridge of an adult dragon.', 0, 0, 50, 0, 0, 0, 0, 0, 0, ''),
(42, 4, 'Dragon''s Claw', 'A claw taken from a dragon.', 0, 25, 0, 0, 0, 0, 0, 0, 0, ''),
(43, 4, 'Dragon''s Tooth', 'A fang ripped from a dragon.', 0, 50, 0, 0, 0, 0, 0, 0, 0, ''),
(44, 4, 'Dragon''s Tear', 'A sad reminder of a dragon''s death.', 0, 0, 0, 50, 0, 0, 0, 0, 0, ''),
(45, 4, 'Dragon''s Wing', 'A flap of leather from a dragon''s wing.', 0, 0, 0, 0, 50, 0, 0, 0, 0, ''),
(46, 4, 'Demon''s Sin', 'The demons sin; sacrificing life for power.', 0, 0, 0, 50, 0, -50, 0, 0, 0, ''),
(47, 4, 'Demon''s Fall', 'The demons failure; sacrificing wisdom for strength.', 0, 0, 0, 50, 0, 0, -50, 0, 0, ''),
(48, 4, 'Demon''s Lie', 'The demons lie; power is more valuable than life.', 0, 0, 0, 100, 0, -100, 0, 0, 0, ''),
(49, 4, 'Demon''s Hate', 'The demons hate; the will of others.', 0, 0, 0, 100, 0, 0, -100, 0, 0, ''),
(50, 4, 'Angel''s Joy', 'The angels joy; the celebration of life.', 0, 0, 0, 25, 0, 25, 0, 0, 0, ''),
(51, 4, 'Angel''s Rise', 'The angels rise; growth begets strength.', 0, 0, 0, 50, 0, 50, 0, 0, 0, ''),
(52, 4, 'Angel''s Truth', 'The angels truth; all breath is valuable.', 0, 0, 0, 75, 0, 75, 0, 0, 0, ''),
(53, 4, 'Angel''s Love', 'The angels love; the protection of existence.', 0, 0, 0, 100, 0, 100, 0, 0, 0, ''),
(54, 4, 'Seraph''s Wisdom', 'The seraphs wisdom; meekness is strength.', 0, 0, 0, 0, 25, 0, 25, 0, 0, ''),
(55, 4, 'Seraph''s Strength', 'The seraphs strength; sharpness of mind and soul.', 0, 0, 0, 0, 50, 0, 50, 0, 0, ''),
(56, 4, 'Seraph''s Creed', 'The seraphs creed; the lust for power makes weak.', 0, 0, 0, 0, 75, 0, 75, 0, 0, ''),
(57, 4, 'Seraph''s Loyalty', 'The seraphs loyalty; to all creation.', 0, 0, 0, 0, 100, 0, 100, 0, 0, ''),
(58, 4, 'Ruby', 'A gorgeous red crystal.', 0, 0, 0, 0, 0, 150, 0, 0, 0, ''),
(59, 4, 'Pearl', 'A beautiful white crystal.', 0, 0, 0, 0, 0, 0, 150, 0, 0, ''),
(60, 4, 'Emerald', 'A mesmerizing green crystal.', 0, 0, 0, 150, 0, 0, 0, 0, 0, ''),
(61, 4, 'Topaz', 'A translucent orange crystal.', 0, 0, 0, 0, 150, 0, 0, 0, 0, ''),
(62, 4, 'Obsidian', 'A razor-sharp black crystal.', 0, 150, 0, 0, 0, 0, 0, 0, 0, ''),
(63, 4, 'Diamond', 'A shockingly tough blue crystal.', 0, 0, 150, 0, 0, 0, 0, 0, 0, ''),
(64, 4, 'Memory Drop', 'This swirling blue liquid comes from the Fount.', 0, 0, 0, 0, 0, 0, 0, 10, 0, ''),
(65, 4, 'Fortune Drop', 'This mysterious green, sparkly concoction comes from lands unknown.', 0, 0, 0, 0, 0, 0, 0, 0, 10, '');

View File

@ -64,6 +64,12 @@
<li>Dexterity: {user.Dexterity}</li> <li>Dexterity: {user.Dexterity}</li>
<li>Attack: {user.Attack}</li> <li>Attack: {user.Attack}</li>
<li>Defense: {user.Defense}</li> <li>Defense: {user.Defense}</li>
{if user.ExpBonus != 0}
<li>EXP Bonus: {user.ExpBonus}%</li>
{/if}
{if user.GoldBonus != 0}
<li>Gold Bonus: {user.GoldBonus}%</li>
{/if}
</ul> </ul>
</section> </section>

View File

@ -36,8 +36,13 @@
{/if} {/if}
</td> </td>
<td> <td>
{item.Att} {if item.HasStatBonuses()}
{if item.Type == 1}Attack{else}Defense{/if} <ul class="unstyled">
{for i,v in item.GetStatBonuses()}
<li>{i}: {v}{if i == "Gold Bonus" or i == "EXP Bonus"}%{/if}</li>
{/for}
</ul>
{/if}
</td> </td>
<td> <td>
{if user.WeaponID == item.ID or user.ArmorID == item.ID or user.ShieldID == item.ID} {if user.WeaponID == item.ID or user.ArmorID == item.ID or user.ShieldID == item.ID}