diff --git a/data/dk.db b/data/dk.db index 5794c06..0fe9890 100644 Binary files a/data/dk.db and b/data/dk.db differ diff --git a/go.mod b/go.mod index 068c392..e8c677a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module dk go 1.25.0 require ( - git.sharkk.net/Sharkk/Sashimi v1.1.3 + git.sharkk.net/Sharkk/Sashimi v1.1.4 git.sharkk.net/Sharkk/Sushi v1.2.0 github.com/valyala/fasthttp v1.65.0 ) diff --git a/go.sum b/go.sum index c2a6716..6bcb246 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.sharkk.net/Sharkk/Sashimi v1.1.3 h1:fY63Zn//A1EffFkoKjCQseRmLFNRibNDZYPUur5SF1s= -git.sharkk.net/Sharkk/Sashimi v1.1.3/go.mod h1:wTMnO6jo34LIjpDJ0qToq14RbwP6Uf4HtdWDmqxrdAM= +git.sharkk.net/Sharkk/Sashimi v1.1.4 h1:aULzzz4Qqpl69Vtpbi7zYYvay4J/HzButYXLwPzB/xw= +git.sharkk.net/Sharkk/Sashimi v1.1.4/go.mod h1:wTMnO6jo34LIjpDJ0qToq14RbwP6Uf4HtdWDmqxrdAM= 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= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= diff --git a/internal/actions/fight.go b/internal/actions/fight.go index c969906..f9066db 100644 --- a/internal/actions/fight.go +++ b/internal/actions/fight.go @@ -2,7 +2,6 @@ package actions import ( "dk/internal/database" - "dk/internal/helpers/exp" "dk/internal/models/fightlogs" "dk/internal/models/fights" "dk/internal/models/monsters" @@ -147,22 +146,24 @@ func HandleSpell(fight *fights.Fight, user *users.User, spellID int) *FightResul } } + fmt.Printf("new MP is %d\n", user.MP-spell.MP) + result := &FightResult{ UserUpdates: map[string]any{"mp": user.MP - spell.MP}, } switch spell.Type { - case spells.TypeHealing: - newHP := min(user.HP+spell.Attribute, user.MaxHP) + case spells.TypeHeal: + newHP := min(user.HP+spell.Power, user.MaxHP) result.UserUpdates["hp"] = newHP - result.ActionText = fmt.Sprintf("You cast %s and healed %d HP!", spell.Name, spell.Attribute) - result.LogAction = func() error { return fightlogs.AddSpellHeal(fight.ID, spell.Name, spell.Attribute) } + result.ActionText = fmt.Sprintf("You cast %s and healed %d HP!", spell.Name, spell.Power) + result.LogAction = func() error { return fightlogs.AddSpellHeal(fight.ID, spell.Name, spell.Power) } - case spells.TypeHurt: - newMonsterHP := max(fight.MonsterHP-spell.Attribute, 0) + case spells.TypeDamage: + newMonsterHP := max(fight.MonsterHP-spell.Power, 0) result.FightUpdates = map[string]any{"monster_hp": newMonsterHP} - result.ActionText = fmt.Sprintf("You cast %s and dealt %d damage!", spell.Name, spell.Attribute) - result.LogAction = func() error { return fightlogs.AddSpellHurt(fight.ID, spell.Name, spell.Attribute) } + result.ActionText = fmt.Sprintf("You cast %s and dealt %d damage!", spell.Name, spell.Power) + result.LogAction = func() error { return fightlogs.AddSpellHurt(fight.ID, spell.Name, spell.Power) } if newMonsterHP <= 0 { monster, err := monsters.Find(fight.MonsterID) @@ -172,6 +173,33 @@ func HandleSpell(fight *fights.Fight, user *users.User, spellID int) *FightResul } } + case spells.TypeSleep: + // For now, sleep spells do damage like the old system + newMonsterHP := max(fight.MonsterHP-spell.Power, 0) + result.FightUpdates = map[string]any{"monster_hp": newMonsterHP} + result.ActionText = fmt.Sprintf("You cast %s and dealt %d damage!", spell.Name, spell.Power) + result.LogAction = func() error { return fightlogs.AddSpellHurt(fight.ID, spell.Name, spell.Power) } + + if newMonsterHP <= 0 { + monster, err := monsters.Find(fight.MonsterID) + if err == nil { + result.EndFightWithVictory(monster, user) + result.ActionText += " " + fmt.Sprintf("%s has been defeated!", monster.Name) + } + } + + case spells.TypeUberAttack: + // Apply attack buff for next attack + result.FightUpdates = map[string]any{"uber_damage": spell.Power} + result.ActionText = fmt.Sprintf("You cast %s! Your next attack will be %d%% stronger!", spell.Name, spell.Power) + result.LogAction = func() error { return fightlogs.AddAction(fight.ID, result.ActionText) } + + case spells.TypeUberDefense: + // Apply defense buff + result.FightUpdates = map[string]any{"uber_defense": spell.Power} + result.ActionText = fmt.Sprintf("You cast %s! You will take %d%% less damage!", spell.Name, spell.Power) + result.LogAction = func() error { return fightlogs.AddAction(fight.ID, result.ActionText) } + default: result.ActionText = "You cast " + spell.Name + " but nothing happened!" result.LogAction = func() error { return fightlogs.AddAction(fight.ID, result.ActionText) } @@ -266,31 +294,6 @@ func HandleMonsterAttack(fight *fights.Fight, user *users.User) *FightResult { return result } -type LevelStats struct { - Strength int - Dexterity int -} - -func calculateLevelUp(currentLevel, newExp, currentStr, currentDex int) (int, LevelStats) { - level := currentLevel - str := currentStr - dex := currentDex - nexp := newExp - - for { - expNeeded := exp.Calc(level + 1) - if nexp < expNeeded { - break - } - level++ - str++ - dex++ - nexp -= expNeeded - } - - return level, LevelStats{Strength: str, Dexterity: dex} -} - func findClosestTown(x, y int) *towns.Town { allTowns, err := towns.All() if err != nil || len(allTowns) == 0 { diff --git a/internal/components/asides.go b/internal/components/asides.go index 92e5a6c..0634112 100644 --- a/internal/components/asides.go +++ b/internal/components/asides.go @@ -2,7 +2,6 @@ package components import ( "dk/internal/helpers" - "dk/internal/models/spells" "dk/internal/models/towns" "dk/internal/models/users" "fmt" @@ -66,11 +65,12 @@ func RightAside(ctx sushi.Ctx) map[string]any { } // Build known healing spells list - if user.Spells != "" { + userSpells, err := user.GetSpells() + if err == nil { spellMap := helpers.NewOrderedMap[int, string]() - for _, id := range user.GetSpellIDs() { - if spell, err := spells.Find(id); err == nil { - spellMap.Set(id, spell.Name) + for _, spell := range userSpells { + if spell.IsHeal() { + spellMap.Set(spell.ID, spell.Name) } } data["_spells"] = spellMap.ToSlice() diff --git a/internal/models/spells/spells.go b/internal/models/spells/spells.go index e37ac1b..c95bb35 100644 --- a/internal/models/spells/spells.go +++ b/internal/models/spells/spells.go @@ -8,29 +8,33 @@ import ( // Spell represents a spell in the game type Spell struct { - ID int - Name string - MP int - Attribute int - Type int + ID int + Type int + Name string + Lore string + Icon string + MP int + Power int } // SpellType constants for spell types const ( - TypeHealing = 1 - TypeHurt = 2 - TypeSleep = 3 - TypeAttackBoost = 4 - TypeDefenseBoost = 5 + TypeHeal = 0 + TypeDamage = 1 + TypeSleep = 2 + TypeUberAttack = 3 + TypeUberDefense = 4 ) // New creates a new Spell with sensible defaults func New() *Spell { return &Spell{ - Name: "", - MP: 5, - Attribute: 10, - Type: TypeHealing, + Type: TypeHeal, + Name: "", + Lore: "", + Icon: "", + MP: 5, + Power: 10, } } @@ -42,10 +46,10 @@ func (s *Spell) Validate() error { if s.MP < 0 { return fmt.Errorf("spell MP cannot be negative") } - if s.Attribute < 0 { - return fmt.Errorf("spell Attribute cannot be negative") + if s.Power < 0 { + return fmt.Errorf("spell Power cannot be negative") } - if s.Type < TypeHealing || s.Type > TypeDefenseBoost { + if s.Type < TypeHeal || s.Type > TypeUberDefense { return fmt.Errorf("invalid spell type: %d", s.Type) } return nil @@ -108,39 +112,83 @@ func ByName(name string) (*Spell, error) { return &spell, nil } -// Helper methods -func (s *Spell) IsHealing() bool { - return s.Type == TypeHealing +// Spell unlock functions +func UnlocksForClassAtLevel(classID, level int) ([]*Spell, error) { + var spells []*Spell + query := ` + SELECT s.* FROM spells s + JOIN spell_unlocks u ON s.id = u.spell_id + WHERE u.class_id = %d AND u.level = %d + ORDER BY s.type ASC, s.mp ASC, s.id ASC` + err := database.Select(&spells, query, classID, level) + return spells, err } -func (s *Spell) IsHurt() bool { - return s.Type == TypeHurt +func UserSpells(userID int) ([]*Spell, error) { + var spells []*Spell + query := ` + SELECT s.* FROM spells s + JOIN user_spells us ON s.id = us.spell_id + WHERE us.user_id = %d + ORDER BY s.type ASC, s.mp ASC, s.id ASC` + err := database.Select(&spells, query, userID) + return spells, err +} + +func GrantSpell(userID, spellID int) error { + return database.Exec("INSERT OR IGNORE INTO user_spells (user_id, spell_id) VALUES (%d, %d)", userID, spellID) +} + +func GrantSpells(userID int, spellIDs []int) error { + return database.Transaction(func() error { + for _, spellID := range spellIDs { + if err := GrantSpell(userID, spellID); err != nil { + return err + } + } + return nil + }) +} + +func HasSpell(userID, spellID int) bool { + var count int + err := database.Get(&count, "SELECT COUNT(*) FROM user_spells WHERE user_id = %d AND spell_id = %d", userID, spellID) + return err == nil && count > 0 +} + +// Helper methods +func (s *Spell) IsHeal() bool { + return s.Type == TypeHeal +} + +func (s *Spell) IsDamage() bool { + return s.Type == TypeDamage } func (s *Spell) IsSleep() bool { return s.Type == TypeSleep } -func (s *Spell) IsAttackBoost() bool { - return s.Type == TypeAttackBoost +func (s *Spell) IsUberAttack() bool { + return s.Type == TypeUberAttack } -func (s *Spell) IsDefenseBoost() bool { - return s.Type == TypeDefenseBoost +func (s *Spell) IsUberDefense() bool { + return s.Type == TypeUberDefense } func (s *Spell) TypeName() string { switch s.Type { - case TypeHealing: - return "Healing" - case TypeHurt: - return "Hurt" + case TypeHeal: + return "Heal" + case TypeDamage: + return "Damage" case TypeSleep: return "Sleep" - case TypeAttackBoost: - return "Attack Boost" - case TypeDefenseBoost: - return "Defense Boost" + case TypeUberAttack: + return "Uber Attack" + case TypeUberDefense: + return "Uber Defense" default: return "Unknown" } @@ -154,13 +202,13 @@ func (s *Spell) Efficiency() float64 { if s.MP == 0 { return 0 } - return float64(s.Attribute) / float64(s.MP) + return float64(s.Power) / float64(s.MP) } func (s *Spell) IsOffensive() bool { - return s.Type == TypeHurt || s.Type == TypeSleep + return s.Type == TypeDamage || s.Type == TypeSleep } func (s *Spell) IsSupport() bool { - return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost + return s.Type == TypeHeal || s.Type == TypeUberAttack || s.Type == TypeUberDefense } diff --git a/internal/models/users/users.go b/internal/models/users/users.go index da68bbe..ef98367 100644 --- a/internal/models/users/users.go +++ b/internal/models/users/users.go @@ -10,6 +10,7 @@ import ( "dk/internal/helpers" "dk/internal/helpers/exp" "dk/internal/models/classes" + "dk/internal/models/spells" ) // User represents a user in the game @@ -208,16 +209,34 @@ func (u *User) IsAlive() bool { return u.HP > 0 } -func (u *User) GetSpellIDs() []int { - return helpers.StringToInts(u.Spells) -} - -func (u *User) SetSpellIDs(spells []int) { - u.Spells = helpers.IntsToString(spells) +func (u *User) GetSpells() ([]*spells.Spell, error) { + return spells.UserSpells(u.ID) } func (u *User) HasSpell(spellID int) bool { - return slices.Contains(u.GetSpellIDs(), spellID) + return spells.HasSpell(u.ID, spellID) +} + +func (u *User) GrantSpell(spellID int) error { + return spells.GrantSpell(u.ID, spellID) +} + +func (u *User) GrantSpells(spellIDs []int) error { + return spells.GrantSpells(u.ID, spellIDs) +} + +func (u *User) LearnNewSpells() error { + newSpells, err := spells.UnlocksForClassAtLevel(u.ClassID, u.Level) + if err != nil { + return err + } + + var spellIDs []int + for _, spell := range newSpells { + spellIDs = append(spellIDs, spell.ID) + } + + return u.GrantSpells(spellIDs) } func (u *User) GetTownIDs() []int { @@ -273,6 +292,7 @@ func (u *User) ExpNeededForNextLevel() int { } func (u *User) GrantExp(expAmount int) map[string]any { + oldLevel := u.Level newLevel, newStr, newDex, newExp := u.CalculateLevelUp(expAmount) updates := map[string]any{ @@ -280,10 +300,18 @@ func (u *User) GrantExp(expAmount int) map[string]any { } // Only include level/stats if they actually changed - if newLevel > u.Level { + if newLevel > oldLevel { updates["level"] = newLevel updates["strength"] = newStr updates["dexterity"] = newDex + + // Learn new spells for each level gained + for level := oldLevel + 1; level <= newLevel; level++ { + if err := u.learnSpellsForLevel(level); err != nil { + // Don't fail the level up if spells fail + fmt.Printf("Failed to grant spells for level %d to user %d: %v\n", level, u.ID, err) + } + } } return updates @@ -333,3 +361,17 @@ func (u *User) Class() *classes.Class { } return class } + +func (u *User) learnSpellsForLevel(level int) error { + newSpells, err := spells.UnlocksForClassAtLevel(u.ClassID, level) + if err != nil { + return err + } + + var spellIDs []int + for _, spell := range newSpells { + spellIDs = append(spellIDs, spell.ID) + } + + return u.GrantSpells(spellIDs) +} diff --git a/internal/routes/auth.go b/internal/routes/auth.go index 936d9ac..7cd6ae9 100644 --- a/internal/routes/auth.go +++ b/internal/routes/auth.go @@ -165,6 +165,12 @@ func processRegister(ctx sushi.Ctx) { return } + // Grant level 1 spells for their class + if err := user.LearnNewSpells(); err != nil { + // Don't fail registration if spells fail, just log it + fmt.Printf("Failed to grant initial spells to user %d: %v\n", user.ID, err) + } + // Auto-login after registration ctx.Login(user.ID, user) diff --git a/internal/routes/fight.go b/internal/routes/fight.go index d40f28c..30f5586 100644 --- a/internal/routes/fight.go +++ b/internal/routes/fight.go @@ -89,13 +89,10 @@ func showFight(ctx sushi.Ctx) { monHpColor = "warning" } - spellMap := helpers.NewOrderedMap[int, *spells.Spell]() - if user.Spells != "" { - for _, id := range user.GetSpellIDs() { - if spell, err := spells.Find(id); err == nil { - spellMap.Set(id, spell) - } - } + var userSpells []*spells.Spell + spellList, err := user.GetSpells() + if err == nil { + userSpells = spellList } // Get recent fight actions @@ -107,7 +104,7 @@ func showFight(ctx sushi.Ctx) { "monster": monster, "mon_hppct": monHpPct, "mon_hpcol": monHpColor, - "spells": spellMap.ToSlice(), + "spells": userSpells, "action": sess.GetFlashMessage("action"), "mon_action": sess.GetFlashMessage("mon_action"), "last_action": lastAction, diff --git a/sql/3_new_spell_system.sql b/sql/3_new_spell_system.sql new file mode 100644 index 0000000..7a86f5a --- /dev/null +++ b/sql/3_new_spell_system.sql @@ -0,0 +1,57 @@ +-- Migration 3: new spell system +-- Created: 2025-08-25 22:13:03 + +DROP TABLE IF EXISTS spells; + +CREATE TABLE spells ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `type` INTEGER NOT NULL DEFAULT 0, + `name` TEXT NOT NULL, + `lore` TEXT DEFAULT '', + `icon` TEXT DEFAULT '', + `mp` INTEGER NOT NULL DEFAULT 0, + `power` INTEGER NOT NULL DEFAULT 0 +); + +-- Types: 0 (Heal), 1 (Damage), 2 (Sleep), 3 (Uber Attack), 4 (Uber Defense) +INSERT INTO spells VALUES +(1, 0, 'Heal', '', '', 5, 10), +(2, 0, 'Revive', '', '', 10, 25), +(3, 0, 'Life', '', '', 25, 50), +(4, 0, 'Breath', '', '', 50, 100), +(5, 0, 'Gaia', '', '', 75, 150), +(6, 1, 'Hurt', '', '', 5, 15), +(7, 1, 'Pain', '', '', 12, 35), +(8, 1, 'Maim', '', '', 25, 70), +(9, 1, 'Rend', '', '', 40, 100), +(10, 1, 'Chaos', '', '', 50, 130), +(11, 2, 'Sleep', '', '', 10, 5), +(12, 2, 'Dream', '', '', 30, 9), +(13, 2, 'Nightmare', '', '', 60, 13), +(14, 3, 'Craze', '', '', 10, 10), +(15, 3, 'Rage', '', '', 20, 25), +(16, 3, 'Fury', '', '', 30, 50), +(17, 4, 'Ward', '', '', 10, 10), +(18, 4, 'Fend', '', '', 20, 25), +(19, 4, 'Barrier', '', '', 30, 50), +(20, 2, 'Spark', 'Small jolt of electric energy.', '', 5, 10), +(21, 2, 'Firebolt', 'Blast of concentrated fire.', '', 10, 30), +(22, 2, 'Geyser', 'Explosion of high-pressure water.', '', 15, 60), +(23, 2, 'Magic Missile', 'Fast, tracking bolt of arcane force.', '', 20, 85); + +CREATE TABLE spell_unlocks ( + `spell_id` INTEGER NOT NULL, + `class_id` INTEGER NOT NULL, + `level` INTEGER NOT NULL +); + +-- Classes: 1 (Adventurer), 2 (Mage), 3 (Warrior), 4 (Paladin) +INSERT INTO spell_unlocks VALUES +(1, 1, 3), (6, 1, 3), (11, 1, 7), (14, 1, 7), (17, 1, 7), +(20, 2, 1), (21, 2, 5), (22, 2, 12), (23, 2, 22), (11, 2, 7), (17, 2, 10), (19, 2, 24), +(1, 4, 1), (2, 4, 5), (3, 4, 10), (4, 4, 20); + +CREATE TABLE user_spells ( + `user_id` INTEGER NOT NULL, + `spell_id` INTEGER NOT NULL +); diff --git a/templates/fight/fight.html b/templates/fight/fight.html index 7199156..02416b4 100644 --- a/templates/fight/fight.html +++ b/templates/fight/fight.html @@ -22,7 +22,7 @@
- {if user.Spells != ""} + {if spells}