new spell system

This commit is contained in:
Sky Johnson 2025-08-26 22:19:43 -05:00
parent 63dabb9e54
commit 2e3a977530
12 changed files with 255 additions and 97 deletions

Binary file not shown.

2
go.mod
View File

@ -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
)

4
go.sum
View File

@ -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=

View File

@ -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 {

View File

@ -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()

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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,

View File

@ -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
);

View File

@ -22,7 +22,7 @@
<div class="mb-05">
<button type="submit" name="action" value="attack" class="btn btn-primary">Attack</button>
</div>
{if user.Spells != ""}
{if spells}
<div class="mb-05">
<select id="spell-select" class="styled-select" name="spell_id">
{for spell in spells}
@ -37,4 +37,4 @@
<button type="submit" name="action" value="run" class="btn">Run</button>
</div>
</form>
{/block}
{/block}

View File

@ -0,0 +1,5 @@
{include "layout.html"}
{block "content"}
<h1>Victory!</h1>
{/block}