create initial fights structure

This commit is contained in:
Sky Johnson 2025-08-14 18:36:59 -05:00
parent b42f4fc983
commit d2d4927314
6 changed files with 425 additions and 78 deletions

View File

@ -34,7 +34,7 @@ func (d Direction) String() string {
func Move(user *users.User, dir Direction) (string, int, int, error) {
control := control.Get()
newX, newY := user.X, user.Y
newX, newY := user.GetPosition()
switch dir {
case North:
newY++

View File

@ -0,0 +1,311 @@
package fights
import (
"dk/internal/store"
"fmt"
"time"
)
// Fight represents a fight, past or present
type Fight struct {
ID int `json:"id"`
UserID int `json:"user_id"`
MonsterID int `json:"monster_id"`
MonsterHP int `json:"monster_hp"`
MonsterMaxHP int `json:"monster_max_hp"`
MonsterSleep int `json:"monster_sleep"`
MonsterImmune int `json:"monster_immune"`
UberDamage int `json:"uber_damage"`
UberDefense int `json:"uber_defense"`
FirstStrike bool `json:"first_strike"`
Turn int `json:"turn"`
RanAway bool `json:"ran_away"`
Victory bool `json:"victory"`
Won bool `json:"won"`
RewardGold int `json:"reward_gold"`
RewardExp int `json:"reward_exp"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
}
func (f *Fight) Save() error {
f.Updated = time.Now().Unix()
return GetStore().UpdateWithRebuild(f.ID, f)
}
func (f *Fight) Delete() error {
GetStore().RemoveWithRebuild(f.ID)
return nil
}
func New(userID, monsterID int) *Fight {
now := time.Now().Unix()
return &Fight{
UserID: userID,
MonsterID: monsterID,
MonsterHP: 0,
MonsterMaxHP: 0,
MonsterSleep: 0,
MonsterImmune: 0,
UberDamage: 0,
UberDefense: 0,
FirstStrike: false,
Turn: 1,
RanAway: false,
Victory: false,
Won: false,
RewardGold: 0,
RewardExp: 0,
Created: now,
Updated: now,
}
}
// Validate checks if fight has valid values
func (f *Fight) Validate() error {
if f.UserID <= 0 {
return fmt.Errorf("fight UserID must be positive")
}
if f.MonsterID <= 0 {
return fmt.Errorf("fight MonsterID must be positive")
}
if f.Turn < 1 {
return fmt.Errorf("fight Turn must be at least 1")
}
if f.MonsterHP < 0 {
return fmt.Errorf("fight MonsterHP cannot be negative")
}
if f.Created <= 0 {
return fmt.Errorf("fight Created timestamp must be positive")
}
if f.Updated <= 0 {
return fmt.Errorf("fight Updated timestamp must be positive")
}
return nil
}
// FightStore with enhanced BaseStore
type FightStore struct {
*store.BaseStore[Fight]
}
// Global store with singleton pattern
var GetStore = store.NewSingleton(func() *FightStore {
fs := &FightStore{BaseStore: store.NewBaseStore[Fight]()}
// Register indices
fs.RegisterIndex("byUserID", store.BuildIntGroupIndex(func(f *Fight) int {
return f.UserID
}))
fs.RegisterIndex("byMonsterID", store.BuildIntGroupIndex(func(f *Fight) int {
return f.MonsterID
}))
fs.RegisterIndex("activeFights", store.BuildFilteredIntGroupIndex(
func(f *Fight) bool {
return !f.RanAway && !f.Victory
},
func(f *Fight) int {
return f.UserID
},
))
fs.RegisterIndex("allByCreated", store.BuildSortedListIndex(func(a, b *Fight) bool {
if a.Created != b.Created {
return a.Created > b.Created // DESC
}
return a.ID > b.ID // DESC
}))
fs.RegisterIndex("allByUpdated", store.BuildSortedListIndex(func(a, b *Fight) bool {
if a.Updated != b.Updated {
return a.Updated > b.Updated // DESC
}
return a.ID > b.ID // DESC
}))
return fs
})
// Enhanced CRUD operations
func (fs *FightStore) AddFight(fight *Fight) error {
return fs.AddWithRebuild(fight.ID, fight)
}
func (fs *FightStore) RemoveFight(id int) {
fs.RemoveWithRebuild(id)
}
func (fs *FightStore) UpdateFight(fight *Fight) error {
return fs.UpdateWithRebuild(fight.ID, fight)
}
// Data persistence
func LoadData(dataPath string) error {
fs := GetStore()
return fs.BaseStore.LoadData(dataPath)
}
func SaveData(dataPath string) error {
fs := GetStore()
return fs.BaseStore.SaveData(dataPath)
}
// Query functions using enhanced store
func Find(id int) (*Fight, error) {
fs := GetStore()
fight, exists := fs.Find(id)
if !exists {
return nil, fmt.Errorf("fight with ID %d not found", id)
}
return fight, nil
}
func All() ([]*Fight, error) {
fs := GetStore()
return fs.AllSorted("allByCreated"), nil
}
func ByUserID(userID int) ([]*Fight, error) {
fs := GetStore()
return fs.GroupByIndex("byUserID", userID), nil
}
func ByMonsterID(monsterID int) ([]*Fight, error) {
fs := GetStore()
return fs.GroupByIndex("byMonsterID", monsterID), nil
}
func ActiveByUserID(userID int) ([]*Fight, error) {
fs := GetStore()
return fs.GroupByIndex("activeFights", userID), nil
}
func Active() ([]*Fight, error) {
fs := GetStore()
result := fs.FilterByIndex("allByCreated", func(f *Fight) bool {
return !f.RanAway && !f.Victory
})
return result, nil
}
func Recent(within time.Duration) ([]*Fight, error) {
fs := GetStore()
cutoff := time.Now().Add(-within).Unix()
result := fs.FilterByIndex("allByCreated", func(f *Fight) bool {
return f.Created >= cutoff
})
return result, nil
}
// Insert with ID assignment
func (f *Fight) Insert() error {
fs := GetStore()
if f.ID == 0 {
f.ID = fs.GetNextID()
}
f.Updated = time.Now().Unix()
return fs.AddFight(f)
}
// Helper methods
func (f *Fight) CreatedTime() time.Time {
return time.Unix(f.Created, 0)
}
func (f *Fight) UpdatedTime() time.Time {
return time.Unix(f.Updated, 0)
}
func (f *Fight) IsActive() bool {
return !f.RanAway && !f.Victory
}
func (f *Fight) IsComplete() bool {
return f.RanAway || f.Victory
}
func (f *Fight) GetStatus() string {
if f.Won {
return "Won"
}
if f.Victory && !f.Won {
return "Lost"
}
if f.RanAway {
return "Ran Away"
}
return "Active"
}
func (f *Fight) GetMonsterHealthPercent() float64 {
if f.MonsterMaxHP <= 0 {
return 0.0
}
return float64(f.MonsterHP) / float64(f.MonsterMaxHP) * 100.0
}
func (f *Fight) IsMonsterSleeping() bool {
return f.MonsterSleep > 0
}
func (f *Fight) IsMonsterImmune() bool {
return f.MonsterImmune > 0
}
func (f *Fight) HasUberBonus() bool {
return f.UberDamage > 0 || f.UberDefense > 0
}
func (f *Fight) GetDuration() time.Duration {
return time.Unix(f.Updated, 0).Sub(time.Unix(f.Created, 0))
}
func (f *Fight) EndFight(victory bool) {
f.Victory = victory
f.RanAway = !victory
f.Updated = time.Now().Unix()
}
func (f *Fight) WinFight(goldReward, expReward int) {
f.Victory = true
f.Won = true
f.RanAway = false
f.RewardGold = goldReward
f.RewardExp = expReward
f.Updated = time.Now().Unix()
}
func (f *Fight) LoseFight() {
f.Victory = true
f.Won = false
f.RanAway = false
f.Updated = time.Now().Unix()
}
func (f *Fight) RunAway() {
f.RanAway = true
f.Victory = false
f.Updated = time.Now().Unix()
}
func (f *Fight) IncrementTurn() {
f.Turn++
f.Updated = time.Now().Unix()
}
func (f *Fight) SetMonsterHP(hp int) {
f.MonsterHP = hp
f.Updated = time.Now().Unix()
}
func (f *Fight) DamageMonster(damage int) {
f.MonsterHP -= damage
if f.MonsterHP < 0 {
f.MonsterHP = 0
}
f.Updated = time.Now().Unix()
}

View File

@ -13,56 +13,49 @@ import (
// User represents a user in the game
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Verified int `json:"verified"`
Token string `json:"token"`
Registered int64 `json:"registered"`
LastOnline int64 `json:"last_online"`
Auth int `json:"auth"`
X int `json:"x"`
Y int `json:"y"`
ClassID int `json:"class_id"`
Currently string `json:"currently"`
Fighting int `json:"fighting"`
MonsterID int `json:"monster_id"`
MonsterHP int `json:"monster_hp"`
MonsterSleep int `json:"monster_sleep"`
MonsterImmune int `json:"monster_immune"`
UberDamage int `json:"uber_damage"`
UberDefense int `json:"uber_defense"`
HP int `json:"hp"`
MP int `json:"mp"`
TP int `json:"tp"`
MaxHP int `json:"max_hp"`
MaxMP int `json:"max_mp"`
MaxTP int `json:"max_tp"`
Level int `json:"level"`
Gold int `json:"gold"`
Exp int `json:"exp"`
GoldBonus int `json:"gold_bonus"`
ExpBonus int `json:"exp_bonus"`
Strength int `json:"strength"`
Dexterity int `json:"dexterity"`
Attack int `json:"attack"`
Defense int `json:"defense"`
WeaponID int `json:"weapon_id"`
ArmorID int `json:"armor_id"`
ShieldID int `json:"shield_id"`
Slot1ID int `json:"slot_1_id"`
Slot2ID int `json:"slot_2_id"`
Slot3ID int `json:"slot_3_id"`
WeaponName string `json:"weapon_name"`
ArmorName string `json:"armor_name"`
ShieldName string `json:"shield_name"`
Slot1Name string `json:"slot_1_name"`
Slot2Name string `json:"slot_2_name"`
Slot3Name string `json:"slot_3_name"`
DropCode int `json:"drop_code"`
Spells string `json:"spells"`
Towns string `json:"towns"`
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Verified int `json:"verified"`
Token string `json:"token"`
Registered int64 `json:"registered"`
LastOnline int64 `json:"last_online"`
Auth int `json:"auth"`
X int `json:"x"`
Y int `json:"y"`
ClassID int `json:"class_id"`
Currently string `json:"currently"`
FightID int `json:"fight_id"`
HP int `json:"hp"`
MP int `json:"mp"`
TP int `json:"tp"`
MaxHP int `json:"max_hp"`
MaxMP int `json:"max_mp"`
MaxTP int `json:"max_tp"`
Level int `json:"level"`
Gold int `json:"gold"`
Exp int `json:"exp"`
GoldBonus int `json:"gold_bonus"`
ExpBonus int `json:"exp_bonus"`
Strength int `json:"strength"`
Dexterity int `json:"dexterity"`
Attack int `json:"attack"`
Defense int `json:"defense"`
WeaponID int `json:"weapon_id"`
ArmorID int `json:"armor_id"`
ShieldID int `json:"shield_id"`
Slot1ID int `json:"slot_1_id"`
Slot2ID int `json:"slot_2_id"`
Slot3ID int `json:"slot_3_id"`
WeaponName string `json:"weapon_name"`
ArmorName string `json:"armor_name"`
ShieldName string `json:"shield_name"`
Slot1Name string `json:"slot_1_name"`
Slot2Name string `json:"slot_2_name"`
Slot3Name string `json:"slot_3_name"`
Spells string `json:"spells"`
Towns string `json:"towns"`
}
func (u *User) Save() error {
@ -86,7 +79,7 @@ func New() *User {
Y: 0,
ClassID: 1,
Currently: "In Town",
Fighting: 0,
FightID: 0,
HP: 10,
MP: 10,
TP: 10,
@ -289,7 +282,7 @@ func (u *User) IsModerator() bool {
}
func (u *User) IsFighting() bool {
return u.Fighting == 1
return u.FightID > 0
}
func (u *User) IsAlive() bool {
@ -333,19 +326,17 @@ func (u *User) GetEquipment() map[string]any {
func (u *User) GetStats() map[string]int {
return map[string]int{
"level": u.Level,
"hp": u.HP,
"mp": u.MP,
"tp": u.TP,
"max_hp": u.MaxHP,
"max_mp": u.MaxMP,
"max_tp": u.MaxTP,
"strength": u.Strength,
"dexterity": u.Dexterity,
"attack": u.Attack,
"defense": u.Defense,
"uber_damage": u.UberDamage,
"uber_defense": u.UberDefense,
"level": u.Level,
"hp": u.HP,
"mp": u.MP,
"tp": u.TP,
"max_hp": u.MaxHP,
"max_mp": u.MaxMP,
"max_tp": u.MaxTP,
"strength": u.Strength,
"dexterity": u.Dexterity,
"attack": u.Attack,
"defense": u.Defense,
}
}

View File

@ -45,7 +45,7 @@ func Move(ctx router.Ctx, _ []string) {
}
user.Currently = currently
user.X, user.Y = newX, newY
user.SetPosition(newX, newY)
if currently == "In Town" {
ctx.Redirect("/town", 303)
@ -95,7 +95,7 @@ func Teleport(ctx router.Ctx, params []string) {
}
user.TP -= town.TPCost
user.X, user.Y = town.X, town.Y
user.SetPosition(town.X, town.Y)
user.Currently = "In Town"
user.Save()

View File

@ -1,9 +1,9 @@
package store
import (
"maps"
"encoding/json"
"fmt"
"maps"
"os"
"path/filepath"
"reflect"
@ -484,3 +484,43 @@ func (bs *BaseStore[T]) SaveData(dataPath string) error {
fmt.Printf("Saved %d items to %s\n", len(bs.items), dataPath)
return nil
}
// BuildFilteredIntGroupIndex creates int-to-[]ID mapping for items passing filter
func BuildFilteredIntGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) int) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[int][]int)
for id, item := range allItems {
if filterFunc(item) {
key := keyFunc(item)
index[key] = append(index[key], id)
}
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}
// BuildFilteredStringGroupIndex creates string-to-[]ID mapping for items passing filter
func BuildFilteredStringGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) string) IndexBuilder[T] {
return func(allItems map[int]*T) any {
index := make(map[string][]int)
for id, item := range allItems {
if filterFunc(item) {
key := keyFunc(item)
index[key] = append(index[key], id)
}
}
// Sort each group by ID
for key := range index {
sort.Ints(index[key])
}
return index
}
}

23
main.go
View File

@ -15,6 +15,7 @@ import (
"dk/internal/models/babble"
"dk/internal/models/control"
"dk/internal/models/drops"
"dk/internal/models/fights"
"dk/internal/models/forum"
"dk/internal/models/items"
"dk/internal/models/monsters"
@ -52,9 +53,7 @@ func main() {
}
}
func loadModels() error {
dataDir := "data"
func loadModels(dataDir string) error {
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
@ -99,12 +98,14 @@ func loadModels() error {
return fmt.Errorf("failed to load control data: %w", err)
}
if err := fights.LoadData(filepath.Join(dataDir, "fights.json")); err != nil {
return fmt.Errorf("failed to load fights data: %w", err)
}
return nil
}
func saveModels() error {
dataDir := "data"
func saveModels(dataDir string) error {
if err := users.SaveData(filepath.Join(dataDir, "users.json")); err != nil {
return fmt.Errorf("failed to save users data: %w", err)
}
@ -145,6 +146,10 @@ func saveModels() error {
return fmt.Errorf("failed to save control data: %w", err)
}
if err := fights.SaveData(filepath.Join(dataDir, "fights.json")); err != nil {
return fmt.Errorf("failed to save fights data: %w", err)
}
return nil
}
@ -163,11 +168,11 @@ func start(port string) error {
template.InitializeCache(cwd)
if err := loadModels(); err != nil {
if err := loadModels(filepath.Join(cwd, "data")); err != nil {
return fmt.Errorf("failed to load models: %w", err)
}
session.Init("data/_sessions.json")
session.Init(filepath.Join(cwd, "data/_sessions.json"))
r := router.New()
r.Use(middleware.Timing())
@ -238,7 +243,7 @@ func start(port string) error {
// Save all model data before shutdown
log.Println("Saving model data...")
if err := saveModels(); err != nil {
if err := saveModels(filepath.Join(cwd, "data")); err != nil {
log.Printf("Error saving model data: %v", err)
}