From 2d958bf8c40069a896156c00132314664fd0e975 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 15 Aug 2025 11:49:39 -0500 Subject: [PATCH] Work on level ups, exp management, rewards, attack scaling, etc --- data/fights.json | 736 +++++++++++++++++++++++++++++++++ data/towns.json | 2 +- internal/actions/fight.go | 39 +- internal/actions/move.go | 5 - internal/components/asides.go | 3 + internal/helpers/exp/exp.go | 5 + internal/models/users/users.go | 42 ++ templates/rightside.html | 2 +- 8 files changed, 821 insertions(+), 13 deletions(-) create mode 100644 internal/helpers/exp/exp.go diff --git a/data/fights.json b/data/fights.json index 9bc95f0..b322ad9 100644 --- a/data/fights.json +++ b/data/fights.json @@ -248,5 +248,741 @@ ], "created": 1755270614, "updated": 1755270620 + }, + { + "id": 5, + "user_id": 1, + "monster_id": 2, + "monster_hp": 0, + "monster_max_hp": 6, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 6, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 1, + "reward_exp": 2, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 11, + "n": "Red Slime" + } + ], + "created": 1755270995, + "updated": 1755271001 + }, + { + "id": 6, + "user_id": 1, + "monster_id": 1, + "monster_hp": 0, + "monster_max_hp": 4, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 4, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 1, + "reward_exp": 1, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Blue Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Blue Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Blue Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 11, + "n": "Blue Slime" + } + ], + "created": 1755274346, + "updated": 1755274352 + }, + { + "id": 7, + "user_id": 1, + "monster_id": 6, + "monster_hp": 10, + "monster_max_hp": 11, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 1, + "ran_away": false, + "victory": true, + "won": false, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 2, + "n": "Drake" + } + ], + "created": 1755274391, + "updated": 1755274393 + }, + { + "id": 8, + "user_id": 1, + "monster_id": 5, + "monster_hp": 6, + "monster_max_hp": 10, + "monster_sleep": 0, + "monster_immune": 1, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": false, + "turn": 4, + "ran_away": false, + "victory": true, + "won": false, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 2, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 3, + "n": "Shadow" + } + ], + "created": 1755274646, + "updated": 1755274652 + }, + { + "id": 9, + "user_id": 1, + "monster_id": 2, + "monster_hp": 0, + "monster_max_hp": 6, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 6, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 1, + "reward_exp": 2, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 11, + "n": "Red Slime" + } + ], + "created": 1755274719, + "updated": 1755274725 + }, + { + "id": 10, + "user_id": 1, + "monster_id": 2, + "monster_hp": 0, + "monster_max_hp": 6, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": false, + "turn": 6, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 1, + "reward_exp": 2, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 11, + "n": "Red Slime" + } + ], + "created": 1755274741, + "updated": 1755274746 + }, + { + "id": 11, + "user_id": 1, + "monster_id": 8, + "monster_hp": 6, + "monster_max_hp": 14, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": false, + "turn": 10, + "ran_away": false, + "victory": true, + "won": false, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 7, + "n": "You failed to run away!" + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 7, + "n": "You failed to run away!" + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + } + ], + "created": 1755274758, + "updated": 1755274768 + }, + { + "id": 12, + "user_id": 1, + "monster_id": 2, + "monster_hp": 0, + "monster_max_hp": 6, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 6, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 1, + "reward_exp": 1, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Red Slime" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 11, + "n": "Red Slime" + } + ], + "created": 1755274777, + "updated": 1755274782 + }, + { + "id": 13, + "user_id": 1, + "monster_id": 8, + "monster_hp": 4, + "monster_max_hp": 14, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 10, + "ran_away": false, + "victory": true, + "won": false, + "reward_gold": 0, + "reward_exp": 0, + "actions": [ + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + }, + { + "t": 1, + "d": 1 + }, + { + "t": 8, + "d": 1, + "n": "Drakelor" + } + ], + "created": 1755274841, + "updated": 1755275436 + }, + { + "id": 14, + "user_id": 1, + "monster_id": 4, + "monster_hp": 0, + "monster_max_hp": 10, + "monster_sleep": 0, + "monster_immune": 0, + "uber_damage": 0, + "uber_defense": 0, + "first_strike": true, + "turn": 5, + "ran_away": false, + "victory": true, + "won": true, + "reward_gold": 1, + "reward_exp": 3, + "actions": [ + { + "t": 1, + "d": 2 + }, + { + "t": 8, + "d": 1, + "n": "Creature" + }, + { + "t": 1, + "d": 2 + }, + { + "t": 8, + "d": 1, + "n": "Creature" + }, + { + "t": 1, + "d": 2 + }, + { + "t": 8, + "d": 1, + "n": "Creature" + }, + { + "t": 1, + "d": 2 + }, + { + "t": 8, + "d": 1, + "n": "Creature" + }, + { + "t": 1, + "d": 2 + }, + { + "t": 11, + "n": "Creature" + } + ], + "created": 1755275442, + "updated": 1755275447 } ] \ No newline at end of file diff --git a/data/towns.json b/data/towns.json index 4ef5c7b..c613127 100644 --- a/data/towns.json +++ b/data/towns.json @@ -4,7 +4,7 @@ "name": "Midworld", "x": 0, "y": 0, - "inn_cost": 5, + "inn_cost": 1, "map_cost": 0, "tp_cost": 0, "shop_list": "1,2,3,17,18,19,28,29" diff --git a/internal/actions/fight.go b/internal/actions/fight.go index 5b757eb..83354e6 100644 --- a/internal/actions/fight.go +++ b/internal/actions/fight.go @@ -19,11 +19,14 @@ func HandleAttack(fight *fights.Fight, user *users.User) { return } - // Player attack damage calculation + // Player attack damage calculation with sqrt scaling attackPower := float64(user.Attack) minAttack := attackPower * 0.75 maxAttack := attackPower - tohit := math.Ceil(rand.Float64()*(maxAttack-minAttack)+minAttack) / 3 + rawAttack := math.Ceil(rand.Float64()*(maxAttack-minAttack) + minAttack) + + // Progressive scaling using square root for smooth progression + tohit := rawAttack / (1.2 + math.Sqrt(attackPower)*0.05) // Critical hit chance based on strength criticalRoll := rand.Intn(150) + 1 @@ -31,11 +34,14 @@ func HandleAttack(fight *fights.Fight, user *users.User) { tohit *= 2 // Critical hit } - // Monster defense calculation + // Monster defense calculation with more aggressive scaling armor := float64(monster.Armor) minBlock := armor * 0.75 maxBlock := armor - toblock := math.Ceil(rand.Float64()*(maxBlock-minBlock)+minBlock) / 3 + rawBlock := math.Ceil(rand.Float64()*(maxBlock-minBlock) + minBlock) + + // Armor uses higher divisor to balance against player attack + toblock := rawBlock / (1.8 + math.Sqrt(armor)*0.08) // Calculate final damage damage := tohit - toblock @@ -58,7 +64,8 @@ func HandleAttack(fight *fights.Fight, user *users.User) { // Check if monster is defeated if fight.MonsterHP <= 0 { fight.AddActionMonsterDeath(monster.Name) - fight.WinFight(fight.RewardGold, fight.RewardExp) + rewardGold, rewardExp := calculateRewards(monster, user) + fight.WinFight(rewardGold, rewardExp) HandleFightWin(fight, user) } } @@ -176,7 +183,7 @@ func HandleMonsterAttack(fight *fights.Fight, user *users.User) { func HandleFightWin(fight *fights.Fight, user *users.User) { // Add rewards to user - user.Exp += fight.RewardExp + user.GrantExp(fight.RewardExp) user.Gold += fight.RewardGold // Reset fight state @@ -226,3 +233,23 @@ func findClosestTown(x, y int) *towns.Town { return closest } + +func calculateRewards(monster *monsters.Monster, user *users.User) (int, int) { + // Base rewards (83-100% of max) + minExp := (monster.MaxExp * 5) / 6 + maxExp := monster.MaxExp + exp := rand.Intn(maxExp-minExp+1) + minExp + + minGold := (monster.MaxGold * 5) / 6 + maxGold := monster.MaxGold + gold := rand.Intn(maxGold-minGold+1) + minGold + + // Apply bonus multipliers + expBonus := (user.ExpBonus * exp) / 100 + exp += expBonus + + goldBonus := (user.GoldBonus * gold) / 100 + gold += goldBonus + + return gold, exp +} diff --git a/internal/actions/move.go b/internal/actions/move.go index 5eca3f1..ae1484d 100644 --- a/internal/actions/move.go +++ b/internal/actions/move.go @@ -60,10 +60,8 @@ func Move(user *users.User, dir Direction) (string, int, int, error) { // 33% chance to start a fight when moving to non-town location if rand.Float32() < 0.33 { - fmt.Println("Fight chance!") monster, err := getRandomMonsterByDistance(newX, newY) if err != nil { - fmt.Printf("Finding a monster failed: %s\n", err.Error()) return "Exploring", newX, newY, nil // Fall back to exploring if monster selection fails } @@ -75,14 +73,11 @@ func Move(user *users.User, dir Direction) (string, int, int, error) { err = fight.Insert() if err != nil { - fmt.Printf("Inserting a fight failed: %s\n", err.Error()) return "Exploring", newX, newY, nil // Fall back if fight creation fails } user.FightID = fight.ID return "Fighting", newX, newY, nil - } else { - fmt.Println("No fight...") } return "Exploring", newX, newY, nil diff --git a/internal/components/asides.go b/internal/components/asides.go index 7be4517..fb2878a 100644 --- a/internal/components/asides.go +++ b/internal/components/asides.go @@ -7,6 +7,7 @@ import ( "dk/internal/models/towns" "dk/internal/models/users" "dk/internal/router" + "fmt" ) // LeftAside generates the data map for the left sidebar. @@ -45,6 +46,8 @@ func RightAside(ctx router.Ctx) map[string]any { user := ctx.UserValue("user").(*users.User) + data["_expprog"] = fmt.Sprintf("%.1f", user.ExpProgress()) + hpPct := helpers.ClampPct(float64(user.HP), float64(user.MaxHP), 0, 100) data["hppct"] = hpPct data["mppct"] = helpers.ClampPct(float64(user.MP), float64(user.MaxMP), 0, 100) diff --git a/internal/helpers/exp/exp.go b/internal/helpers/exp/exp.go new file mode 100644 index 0000000..aa52d4b --- /dev/null +++ b/internal/helpers/exp/exp.go @@ -0,0 +1,5 @@ +package exp + +func Calc(level int) int { + return level * level * level +} diff --git a/internal/models/users/users.go b/internal/models/users/users.go index 41e5140..95df143 100644 --- a/internal/models/users/users.go +++ b/internal/models/users/users.go @@ -1,6 +1,7 @@ package users import ( + "dk/internal/helpers/exp" "dk/internal/store" "fmt" "slices" @@ -348,3 +349,44 @@ func (u *User) SetPosition(x, y int) { u.X = x u.Y = y } + +func (u *User) ExpNeededForNextLevel() int { + return exp.Calc(u.Level + 1) +} + +func (u *User) GrantExp(expAmount int) { + u.Exp += expAmount + u.checkLevelUp() +} + +func (u *User) checkLevelUp() { + expNeeded := u.ExpNeededForNextLevel() + + if u.Exp >= expNeeded { + // Level up + u.Level++ + u.Strength++ + u.Dexterity++ + + // Reset exp and carry over excess + excessExp := u.Exp - expNeeded + u.Exp = 0 + + // Recursive level up if enough excess exp + if excessExp > 0 { + u.GrantExp(excessExp) + } + } +} + +func (u *User) ExpProgress() float64 { + if u.Level == 1 { + return float64(u.Exp) / float64(u.ExpNeededForNextLevel()) * 100 + } + + currentLevelExp := exp.Calc(u.Level) + nextLevelExp := u.ExpNeededForNextLevel() + progressExp := u.Exp + + return float64(progressExp) / float64(nextLevelExp-currentLevelExp) * 100 +} diff --git a/templates/rightside.html b/templates/rightside.html index d41ba18..3f14b5b 100644 --- a/templates/rightside.html +++ b/templates/rightside.html @@ -3,7 +3,7 @@