From 2e3a97753068ef0cb20555873b93417f3ff4888f Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 26 Aug 2025 22:19:43 -0500 Subject: [PATCH] new spell system --- data/dk.db | Bin 81920 -> 94208 bytes go.mod | 2 +- go.sum | 4 +- internal/actions/fight.go | 71 +++++++++--------- internal/components/asides.go | 10 +-- internal/models/spells/spells.go | 122 +++++++++++++++++++++---------- internal/models/users/users.go | 58 +++++++++++++-- internal/routes/auth.go | 6 ++ internal/routes/fight.go | 13 ++-- sql/3_new_spell_system.sql | 57 +++++++++++++++ templates/fight/fight.html | 4 +- templates/fight/victory.html | 5 ++ 12 files changed, 255 insertions(+), 97 deletions(-) create mode 100644 sql/3_new_spell_system.sql create mode 100644 templates/fight/victory.html diff --git a/data/dk.db b/data/dk.db index 5794c0601ab878b9e130d38534eb330ae48310d9..0fe9890b9bd836b8d91886576eb574f616ced542 100644 GIT binary patch delta 2016 zcmcgtT}&KR6rTS-m^lk9Ot++T5GvHdQnoBL5;)6fwLp45#(Kcx{jrT6yrZql$lY8$u z-~G-#XXc)J?uirpi68klDoc2ZqIBE~t@KpGj&hy~zxoteVc-r~sDQuV&SifIHee-C zj0aFuJhEU>a0AYOwNesz5t^G=o@(65Wzg)nmqoTc?#+2VO79r|Hxl-y?bLF(Gn^U$spvDI|anoRlQwlnFmvciVs zyzWUdtIIPUay?mKb=r4kGoGYKY>3Xs;^`~bJ3n{w-g1|wBf5H%f;IRazJbr+eK-rR z!*Q5`gU}DHuo+bCZ|zs@JMAm&W9=1fOsiIJ;s}=2g?3${=(;hLrkTCwq!l;zTApXw zsL%BL2E%vF_>`5IG!8qqZ#YTAbmL|Q8A->Dqi}F6Qh}{Q!^FBD&Ea5sk7aGgvz!be zr|^e0W?kfYGcGb_O&_^oXb+cTn@v5$rq5b%5!t%i#_oto+{wgI2D|l9!bn;!3M*CN zAU4o<6NAyL=}wKNP1`n(JctO{DDJyBaFjvrqD7DFa#xeV1cmqn{Gl;AW)#NeKYPHwtIhfW`Zz7 zF|a*>&?k^@B#kx?6=tilHriIQ~{`)7JmrsGAZB~8XqKE@~xu|A#Ze;W|m>I9ZbD zN{P-lnWiNP544PDNz&_7(jbA-POa%qI+=)-s6E=zyt6jl(bn3!qpiX3Xl}@MwA4nX zts{H7=OZm$%|rI|z_yWNPC7DD>kafD9&H&M+tzkq`)Hyqo=q+6N+tU<`;*aumhHn+ z(_a5R&wrt3FdB7;muJZkjyq|56YEK5c4v$jzBV%8!&ORHkx|MyErAV3&zl| zA~!8m7AspxC8J@Lg4LB0;~I2F=rSsdJ2@L^OJ0!jMJ zuZ9;X_!h3gMZC{>h?cS<*+a+l{54E1Hzk7%!Q@Sx)I%~1Uvq5z(vvhnr7li9=*$=i zCsXGeQyFL0@KdI5)N zLIqPSibN=9ctOB|9ApHZ!wFm>3LKd=QDDhjBnk|fg^esvbNHvRBx+u1q_nS#<3)Rv zca=sQU!}5VzC_e(YUstMeeTl+lw4Zgh!g66NLxnPx)NG@i_Jg7G~A=$9!|R7K%_XDmFeI= DRN~$? delta 600 zcmXw$&ubGw6vt;~XE)hRX5OYr3`-$dP((`c5Q`_F*h(x|gH(-r9LU%#Y|d9BK`p_NTE0JAiW4h5Wy-q>DGby@%et=n>WL2f6?1*{lRd8 zB7{Sy&1K1&B4t%S*{^$k3ON6>*KsK9lYf-yJ-X)yRNzRS0Rg#DZ^+erfrV~i(+ z?|ahE=5;n?ptMp7vR+O(I!lCjP55QY(wTkeF8E8?JY^8#jF^=vhjMP9jLMz##cgR> zbch)!r?}|NR$4XT%QH;l@i69`FGX!RYic|e3YV)=&Z(b)HDH1#E27cNqZPu;!y@WM zC(49HL%b-wno}Sw65=J{)#FDAGeVqN@LM_%rVo&q=Y=OOpog3q$idj0P{mu7eEE1> zI|w(Yy_?d>a}wG!a?x9qmJ`RJ8~lE6*#UL`LWY%4)L2JQ 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}