Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions _datafiles/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,27 @@ Validation:
- "join"
- "register"

################################################################################
#
# Statistics Configuration
# Settings for character statistics and stat calculations
#
################################################################################
Statistics:
# Base stat values for new characters
BaseStats:
Strength: 1 # Muscular strength
Speed: 1 # Speed and agility
Smarts: 1 # Intelligence and wisdom
Vitality: 1 # Health and stamina
Mysticism: 1 # Magic and mana
Perception: 1 # How well you notice things

# Stat calculation factors
Factors:
BaseModFactor: 0.3333333334 # How much of a scaling to apply to levels before multiplying by racial stat
NaturalGainsModFactor: 0.5 # Free stats gained per level modded by this

################################################################################
#
# Roles
Expand Down
12 changes: 6 additions & 6 deletions _datafiles/html/admin/races/race.data.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,41 +70,41 @@ <h3>Base Stats</h3>
<div class="input-group-prepend w-50">
<span class="input-group-text w-100" id="stats-strength">Strength</span>
</div>
<input type="text" name="stats[strength]" class="form-control" value="{{.raceInfo.Stats.Strength.Base}}">
<input type="text" name="stats[strength]" class="form-control" value="{{.raceInfo.Stats.GetBase "Strength"}}">
</div>

<div class="input-group col">
<div class="input-group-prepend w-50">
<span class="input-group-text w-100" id="stats-speed">Speed</span>
</div>
<input type="text" name="stats[speed]" class="form-control" value="{{.raceInfo.Stats.Speed.Base}}">
<input type="text" name="stats[speed]" class="form-control" value="{{.raceInfo.Stats.GetBase "Speed"}}">
</div>

<div class="input-group col">
<div class="input-group-prepend w-50">
<span class="input-group-text w-100" id="stats-smarts">Smarts</span>
</div>
<input type="text" name="stats[smarts]" class="form-control" value="{{.raceInfo.Stats.Smarts.Base}}">
<input type="text" name="stats[smarts]" class="form-control" value="{{.raceInfo.Stats.GetBase "Smarts"}}">
</div>
<div class="input-group col">
<div class="input-group-prepend w-50">
<span class="input-group-text w-100" id="stats-vitality">Vitality</span>
</div>
<input type="text" name="stats[vitality]" class="form-control" value="{{.raceInfo.Stats.Vitality.Base}}">
<input type="text" name="stats[vitality]" class="form-control" value="{{.raceInfo.Stats.GetBase "Vitality"}}">
</div>

<div class="input-group col">
<div class="input-group-prepend w-50">
<span class="input-group-text w-100" id="stats-mysticism">Mysticism</span>
</div>
<input type="text" name="stats[mysticism]" class="form-control" value="{{.raceInfo.Stats.Mysticism.Base}}">
<input type="text" name="stats[mysticism]" class="form-control" value="{{.raceInfo.Stats.GetBase "Mysticism"}}">
</div>

<div class="input-group col">
<div class="input-group-prepend w-50">
<span class="input-group-text w-100" id="stats-perception">Perception</span>
</div>
<input type="text" name="stats[perception]" class="form-control" value="{{.raceInfo.Stats.Perception.Base}}">
<input type="text" name="stats[perception]" class="form-control" value="{{.raceInfo.Stats.GetBase "Perception"}}">
</div>

</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
{{- $hpDisplay := printf "%s" ( healthStr .Character.Health .Character.HealthMax.Value 22 ) }}
{{- $mpDisplay := printf "%s" ( manaStr .Character.Mana .Character.ManaMax.Value 22 ) }}
┌─ <ansi fg="black-bold">.:</ansi><ansi fg="20">Info</ansi> ──────────────────────┐ ┌─ <ansi fg="black-bold">.:</ansi><ansi fg="20">Attributes</ansi> ───────────────────────────┐
│ <ansi fg="yellow">Area: </ansi>{{ printf "%-22s" .Character.Zone }}│ │ <ansi fg="yellow">Strength: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" .Character.Stats.Strength.Value (.Character.StatMod "strength") }} <ansi fg="yellow">Vitality: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" .Character.Stats.Vitality.Value (.Character.StatMod "vitality") }} │
<ansi fg="yellow">Race: </ansi>{{ printf "%-22s" .Character.Race }} <ansi fg="yellow">Speed: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" .Character.Stats.Speed.Value (.Character.StatMod "speed") }} <ansi fg="yellow">Mysticism: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" .Character.Stats.Mysticism.Value (.Character.StatMod "mysticism") }}
<ansi fg="yellow">Level: </ansi>{{ printf "%-22d" .Character.Level }} │ <ansi fg="yellow">Smarts: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" .Character.Stats.Smarts.Value (.Character.StatMod "smarts") }} <ansi fg="yellow">Percept: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" .Character.Stats.Perception.Value (.Character.StatMod "perception") }} │
│ <ansi fg="yellow">Area: </ansi>{{ printf "%-22s" .Character.Zone }}│ │ <ansi fg="yellow">Strength: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" (charStatValue .Character "strength") (.Character.StatMod "strength") }} <ansi fg="yellow">Vitality: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" (charStatValue .Character "Vitality") (.Character.StatMod "vitality") }} │
<ansi fg="yellow">Race: </ansi>{{ printf "%-22s" .Character.Race }} <ansi fg="yellow">Speed: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" (charStatValue .Character "Speed") (.Character.StatMod "speed") }} <ansi fg="yellow">Mysticism: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" (charStatValue .Character "Mysticism") (.Character.StatMod "mysticism") }}
<ansi fg="yellow">Level: </ansi>{{ printf "%-22d" .Character.Level }} │ <ansi fg="yellow">Smarts: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" (charStatValue .Character "Smarts") (.Character.StatMod "smarts") }} <ansi fg="yellow">Percept: </ansi>{{ printf "<ansi fg=\"stat\">%-4d</ansi><ansi fg=\"statmod\">(%-3d)</ansi>" (charStatValue .Character "Perception") (.Character.StatMod "perception") }} │
<ansi fg="yellow">Exp: </ansi>{{ printf "%-22s" ( tnl .UserId ) }} └──────────────────────────────────────────┘
<ansi fg="yellow">Health: </ansi>{{ printf "%s" $hpDisplay }} ┌─ <ansi fg="black-bold">.:</ansi><ansi fg="20">Wealth</ansi> ────────┐ ┌─ <ansi fg="black-bold">.:</ansi><ansi fg="20">Training</ansi> ───────┐
<ansi fg="yellow">Mana: </ansi>{{ printf "%s" $mpDisplay }} │ <ansi fg="yellow">Gold: </ansi>{{ printf "%-11s" (numberFormat .Character.Gold) }} │ │ <ansi fg="yellow">Train Pts:</ansi> {{ printf "%-7d" .Character.TrainingPoints }} │
│ <ansi fg="yellow">Armor: </ansi>{{ printf "%-6s" ( printf "%d" (.Character.GetDefense)) }} {{ if permadeath }}<ansi fg="yellow">Lives: </ansi>{{ printf "%-7d" .Character.ExtraLives }}{{ else }} {{ end }} │ │ <ansi fg="yellow">Bank: </ansi>{{ printf "%-11s" (numberFormat .Character.Bank) }} │ │ <ansi fg="yellow">Stat Pts:</ansi> {{ printf "%-7d" .Character.StatPoints }} │
└───────────────────────────────┘ └───────────────────┘ └────────────────────┘
{{- if gt .Character.StatPoints 0 }}{{ if lt .Character.Level 5 }}
<ansi fg="alert-5">TIP:</ansi> <ansi fg="alert-2">Type <ansi fg="command">status train</ansi> to spend stat points on improvements.</ansi> {{ end }}{{ end -}}
<ansi fg="alert-5">TIP:</ansi> <ansi fg="alert-2">Type <ansi fg="command">status train</ansi> to spend stat points on improvements.</ansi> {{ end }}{{ end -}}
123 changes: 56 additions & 67 deletions internal/characters/character.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,21 @@ type Character struct {
}

func New() *Character {
statsConfig := configs.GetStatisticsConfig()

return &Character{
//Name: defaultName,
Adjectives: []string{},
RoomId: StartingRoomId,
Zone: startingZone,
RaceId: startingRace,
Stats: stats.Statistics{
Strength: stats.StatInfo{Base: 1},
Speed: stats.StatInfo{Base: 1},
Smarts: stats.StatInfo{Base: 1},
Vitality: stats.StatInfo{Base: 1},
Mysticism: stats.StatInfo{Base: 1},
Perception: stats.StatInfo{Base: 1},
Strength: stats.StatInfo{Base: int(statsConfig.BaseStats.Strength)},
Speed: stats.StatInfo{Base: int(statsConfig.BaseStats.Speed)},
Smarts: stats.StatInfo{Base: int(statsConfig.BaseStats.Smarts)},
Vitality: stats.StatInfo{Base: int(statsConfig.BaseStats.Vitality)},
Mysticism: stats.StatInfo{Base: int(statsConfig.BaseStats.Mysticism)},
Perception: stats.StatInfo{Base: int(statsConfig.BaseStats.Perception)},
},
Level: 1,
Experience: 1,
Expand Down Expand Up @@ -206,7 +208,7 @@ func (c *Character) GetBaseCastSuccessChance(spellId string) int {
}
targetNumber += proficiency

targetNumber += int(math.Floor(float64(c.Stats.Mysticism.ValueAdj) / 5))
targetNumber += int(math.Floor(float64(c.Stats.Get("Mysticism").ValueAdj) / 5))
Copy link
Preview

Copilot AI Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling Get("Mysticism") inside hot loops involves string lookups and a switch; consider caching the StatInfo pointer in a local variable to reduce overhead.

Suggested change
targetNumber += int(math.Floor(float64(c.Stats.Get("Mysticism").ValueAdj) / 5))
mysticismStat := c.Stats.Get("Mysticism")
targetNumber += int(math.Floor(float64(mysticismStat.ValueAdj) / 5))

Copilot uses AI. Check for mistakes.


// add by any stat mods for casting, or casting school
// 0-xx
Expand All @@ -222,7 +224,7 @@ func (c *Character) GetBaseCastSuccessChance(spellId string) int {
}

func (c *Character) CarryCapacity() int {
return 5 + c.Stats.Strength.ValueAdj/3
return 5 + c.Stats.Get("Strength").ValueAdj/3
}

func (c *Character) DeductActionPoints(amount int) bool {
Expand Down Expand Up @@ -367,9 +369,9 @@ func (c *Character) GetDefaultDiceRoll() (attacks int, dCount int, dSides int, b
bonus = raceInfo.Damage.BonusDamage
buffOnCrit = raceInfo.Damage.CritBuffIds

dCount += int(math.Floor((float64(c.Stats.Speed.ValueAdj) / 50)))
dSides += int(math.Floor((float64(c.Stats.Strength.ValueAdj) / 12)))
bonus += int(math.Floor((float64(c.Stats.Perception.ValueAdj) / 25)))
dCount += int(math.Floor((float64(c.Stats.Get("Speed").ValueAdj) / 50)))
dSides += int(math.Floor((float64(c.Stats.Get("Strength").ValueAdj) / 12)))
bonus += int(math.Floor((float64(c.Stats.Get("Perception").ValueAdj) / 25)))

if dCount < raceInfo.Damage.DiceCount {
dCount = raceInfo.Damage.DiceCount
Expand Down Expand Up @@ -958,15 +960,15 @@ func (c *Character) GetMaxCharmedCreatures() int {
}

func (c *Character) GetMemoryCapacity() int {
memCap := c.GetSkillLevel(skills.Map) * c.Stats.Smarts.ValueAdj
memCap := c.GetSkillLevel(skills.Map) * c.Stats.Get("Smarts").ValueAdj
if memCap < 0 {
memCap = 0
}
return memCap + 5
}

func (c *Character) GetMapSprawlCapacity() int {
sprawlCap := c.GetSkillLevel(skills.Map) + (c.Stats.Smarts.ValueAdj >> 2)
sprawlCap := c.GetSkillLevel(skills.Map) + (c.Stats.Get("Smarts").ValueAdj >> 2)
if sprawlCap < 0 {
sprawlCap = 0
}
Expand Down Expand Up @@ -1256,7 +1258,7 @@ func (c *Character) ApplyManaChange(manaChange int) int {
}

func (c *Character) BarterPrice(startPrice int) int {
factor := (float64(c.Stats.Perception.ValueAdj) / 3) / 100 // 100 = 33% discount, 0 = 0% discount, 300 = 100% discount
factor := (float64(c.Stats.Get("Perception").ValueAdj) / 3) / 100 // 100 = 33% discount, 0 = 0% discount, 300 = 100% discount
if factor > .75 {
factor = .75
}
Expand Down Expand Up @@ -1308,12 +1310,12 @@ func (c *Character) LevelUp() (bool, stats.Statistics) {

var statsDelta stats.Statistics = c.Stats

statsDelta.Strength.Value -= statsBefore.Strength.Value
statsDelta.Speed.Value -= statsBefore.Speed.Value
statsDelta.Smarts.Value -= statsBefore.Smarts.Value
statsDelta.Vitality.Value -= statsBefore.Vitality.Value
statsDelta.Mysticism.Value -= statsBefore.Mysticism.Value
statsDelta.Perception.Value -= statsBefore.Perception.Value
statsDelta.Get("Strength").Value -= statsBefore.Get("Strength").Value
statsDelta.Get("Speed").Value -= statsBefore.Get("Speed").Value
statsDelta.Get("Smarts").Value -= statsBefore.Get("Smarts").Value
statsDelta.Get("Vitality").Value -= statsBefore.Get("Vitality").Value
statsDelta.Get("Mysticism").Value -= statsBefore.Get("Mysticism").Value
statsDelta.Get("Perception").Value -= statsBefore.Get("Perception").Value

c.Health = c.HealthMax.Value
c.Mana = c.ManaMax.Value
Expand All @@ -1340,7 +1342,7 @@ func (c *Character) Heal(hp int, mana int) (int, int) {
func (c *Character) HealthPerRound() int {
return 1 + c.StatMod(string(statmods.HealthRecovery))
/*
healAmt := math.Round(float64(c.Stats.Vitality.ValueAdj)/8) +
healAmt := math.Round(float64(c.Stats.Get("Vitality").ValueAdj)/8) +
math.Round(float64(c.Level)/12) +
1.0

Expand All @@ -1351,7 +1353,7 @@ func (c *Character) HealthPerRound() int {
func (c *Character) ManaPerRound() int {
return 1 + c.StatMod(string(statmods.ManaRecovery))
/*
healAmt := math.Round(float64(c.Stats.Mysticism.ValueAdj)/8) +
healAmt := math.Round(float64(c.Stats.Get("Mysticism").ValueAdj)/8) +
math.Round(float64(c.Level)/12) +
1.0

Expand All @@ -1361,9 +1363,9 @@ func (c *Character) ManaPerRound() int {

// Where 1000 = a full round
func (c *Character) MovementCost() int {
modifier := 3 // by default they should be able to move 3 times per round.
modifier += int(c.Level / 15) // Every 15 levels, get an extra movement.
modifier += int(c.Stats.Speed.ValueAdj / 15) // Every 15 speed, get an extra movement
modifier := 3 // by default they should be able to move 3 times per round.
modifier += int(c.Level / 15) // Every 15 levels, get an extra movement.
modifier += int(c.Stats.Get("Speed").ValueAdj / 15) // Every 15 speed, get an extra movement
return int(1000 / modifier)
}

Expand All @@ -1372,6 +1374,8 @@ func (c *Character) StatMod(statName string) int {
}

// returns true if something has changed.
// TODO: [nitpick] There are many repetitive Get("X") calls; consider iterating over a slice of stat names or using a helper to apply updates in a loop for readability.
Copy link
Preview

Copilot AI Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Implement the suggested refactoring by looping over Statistics.GetStatInfoNames() and applying the same operations to each stat, reducing boilerplate and potential copy-paste errors.

Copilot uses AI. Check for mistakes.


func (c *Character) RecalculateStats() {

// Make sure racial base stats are set
Expand All @@ -1385,43 +1389,34 @@ func (c *Character) RecalculateStats() {
if c.TNLScale == 0 {
c.TNLScale = 1.0
}
c.Stats.Strength.Base = raceInfo.Stats.Strength.Base
c.Stats.Speed.Base = raceInfo.Stats.Speed.Base
c.Stats.Smarts.Base = raceInfo.Stats.Smarts.Base
c.Stats.Vitality.Base = raceInfo.Stats.Vitality.Base
c.Stats.Mysticism.Base = raceInfo.Stats.Mysticism.Base
c.Stats.Perception.Base = raceInfo.Stats.Perception.Base
for _, statName := range c.Stats.GetStatInfoNames() {
c.Stats.Get(statName).Base = raceInfo.Stats.Get(statName).Base
}
}

// Add any mods for equipment
c.Stats.Strength.Mods = c.StatMod(string(statmods.Strength))
c.Stats.Speed.Mods = c.StatMod(string(statmods.Speed))
c.Stats.Smarts.Mods = c.StatMod(string(statmods.Smarts))
c.Stats.Vitality.Mods = c.StatMod(string(statmods.Vitality))
c.Stats.Mysticism.Mods = c.StatMod(string(statmods.Mysticism))
c.Stats.Perception.Mods = c.StatMod(string(statmods.Perception))
for _, statName := range c.Stats.GetStatInfoNames() {
c.Stats.Get(statName).Mods = c.StatMod(statName)
}

// Recalculate stats
// Stats are basically:
// level*base + training + mods
c.Stats.Strength.Recalculate(c.Level)
c.Stats.Speed.Recalculate(c.Level)
c.Stats.Smarts.Recalculate(c.Level)
c.Stats.Vitality.Recalculate(c.Level)
c.Stats.Mysticism.Recalculate(c.Level)
c.Stats.Perception.Recalculate(c.Level)
for _, statName := range c.Stats.GetStatInfoNames() {
c.Stats.Get(statName).Recalculate(c.Level)
}

// Set HP/MP maxes
// This relies on the above stats so has to be calculated afterwards
c.HealthMax.Mods = 5 +
c.StatMod(string(statmods.HealthMax)) + // Any sort of spell buffs etc. are just direct modifiers
c.Level + // For every level you get 1 hp
c.Stats.Vitality.ValueAdj*4 // for every vitality you get 3hp
c.Stats.Get("Vitality").ValueAdj*4 // for every vitality you get 3hp

c.ManaMax.Mods = 4 +
c.StatMod(string(statmods.ManaMax)) + // Any sort of spell buffs etc. are just direct modifiers
c.Level + // For every level you get 1 mp
c.Stats.Mysticism.ValueAdj*3 // for every Mysticism you get 2mp
c.Stats.Get("Mysticism").ValueAdj*3 // for every Mysticism you get 2mp

// Set max action points
c.ActionPointsMax.Mods = 200 // hard coded for now
Expand All @@ -1445,22 +1440,16 @@ func (c *Character) RecalculateStats() {
if c.userId != 0 {
changed := false
// return true if something has changed.
if beforeStats.Strength.ValueAdj != c.Stats.Strength.ValueAdj {
changed = true
} else if beforeStats.Speed.ValueAdj != c.Stats.Speed.ValueAdj {
changed = true
} else if beforeStats.Smarts.ValueAdj != c.Stats.Smarts.ValueAdj {
changed = true
} else if beforeStats.Vitality.ValueAdj != c.Stats.Vitality.ValueAdj {
changed = true
} else if beforeStats.Mysticism.ValueAdj != c.Stats.Mysticism.ValueAdj {
changed = true
} else if beforeStats.Perception.ValueAdj != c.Stats.Perception.ValueAdj {
changed = true
} else if beforeHealthMax != c.HealthMax {
changed = true
} else if beforeManaMax != c.ManaMax {
changed = true
for _, statName := range c.Stats.GetStatInfoNames() {
if beforeStats.Get(statName).ValueAdj != c.Stats.Get(statName).ValueAdj {
changed = true
break
}
}
if !changed {
if beforeHealthMax != c.HealthMax || beforeManaMax != c.ManaMax {
changed = true
}
}

if changed {
Expand All @@ -1481,17 +1470,17 @@ func (c *Character) AutoTrain() {

switch util.Rand(6) {
case 0:
c.Stats.Strength.Training++
c.Stats.Get("Strength").Training++
case 1:
c.Stats.Speed.Training++
c.Stats.Get("Speed").Training++
case 2:
c.Stats.Smarts.Training++
c.Stats.Get("Smarts").Training++
case 3:
c.Stats.Vitality.Training++
c.Stats.Get("Vitality").Training++
case 4:
c.Stats.Mysticism.Training++
c.Stats.Get("Mysticism").Training++
case 5:
c.Stats.Perception.Training++
c.Stats.Get("Perception").Training++
}

c.StatPoints--
Expand Down
2 changes: 1 addition & 1 deletion internal/characters/character_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func TestCharacter_CarryCapacity(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := New()
c.Stats.Strength.ValueAdj = tt.strengthAdj
c.Stats.Get("Strength").ValueAdj = tt.strengthAdj
got := c.CarryCapacity()
assert.Equal(t, tt.expectedCap, got)
})
Expand Down
4 changes: 2 additions & 2 deletions internal/combat/calculations.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ func PowerRanking(atkChar characters.Character, defChar characters.Character) fl
pct += 0.4 * float64(atkDmg) / float64(defDmg)
}

if defChar.Stats.Speed.ValueAdj == 0 {
if defChar.Stats.Get("Speed").ValueAdj == 0 {
pct += 0.3
} else {
pct += 0.3 * float64(atkChar.Stats.Speed.ValueAdj) / float64(defChar.Stats.Speed.ValueAdj)
pct += 0.3 * float64(atkChar.Stats.Get("Speed").ValueAdj) / float64(defChar.Stats.Get("Speed").ValueAdj)
}

if defChar.HealthMax.Value == 0 {
Expand Down
Loading