2024-06-06 23:17:22 +03:00
|
|
|
package model
|
2024-05-21 23:08:51 +03:00
|
|
|
|
|
|
|
import (
|
|
|
|
"math/rand"
|
2024-06-01 11:20:51 +03:00
|
|
|
"mvvasilev/last_light/engine"
|
2024-05-21 23:08:51 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
type Stat int
|
|
|
|
|
|
|
|
const (
|
|
|
|
// Used as a default value in cases where no other stat could be determined.
|
|
|
|
// Should never be used except in cases of error handling
|
|
|
|
Stat_NonExtant Stat = -1
|
|
|
|
|
|
|
|
Stat_Attributes_Strength Stat = 0
|
|
|
|
Stat_Attributes_Dexterity Stat = 10
|
|
|
|
Stat_Attributes_Intelligence Stat = 20
|
|
|
|
Stat_Attributes_Constitution Stat = 30
|
|
|
|
|
|
|
|
Stat_PhysicalPrecisionBonus Stat = 5
|
|
|
|
Stat_EvasionBonus Stat = 15
|
|
|
|
Stat_MagicPrecisionBonus Stat = 25
|
|
|
|
Stat_TotalPrecisionBonus Stat = 35
|
|
|
|
|
|
|
|
Stat_DamageBonus_Physical_Unarmed Stat = 40
|
|
|
|
Stat_DamageBonus_Physical_Slashing Stat = 50
|
|
|
|
Stat_DamageBonus_Physical_Piercing Stat = 60
|
|
|
|
Stat_DamageBonus_Physical_Bludgeoning Stat = 70
|
|
|
|
|
|
|
|
Stat_DamageBonus_Magic_Fire Stat = 80
|
|
|
|
Stat_DamageBonus_Magic_Cold Stat = 90
|
|
|
|
Stat_DamageBonus_Magic_Necrotic Stat = 100
|
|
|
|
Stat_DamageBonus_Magic_Thunder Stat = 110
|
|
|
|
Stat_DamageBonus_Magic_Acid Stat = 120
|
|
|
|
Stat_DamageBonus_Magic_Poison Stat = 130
|
|
|
|
|
2024-06-06 23:17:22 +03:00
|
|
|
Stat_ResistanceBonus_Physical_Unarmed Stat = 200
|
|
|
|
Stat_ResistanceBonus_Physical_Slashing Stat = 210
|
|
|
|
Stat_ResistanceBonus_Physical_Piercing Stat = 220
|
|
|
|
Stat_ResistanceBonus_Physical_Bludgeoning Stat = 230
|
|
|
|
|
|
|
|
Stat_ResistanceBonus_Magic_Fire Stat = 240
|
|
|
|
Stat_ResistanceBonus_Magic_Cold Stat = 250
|
|
|
|
Stat_ResistanceBonus_Magic_Necrotic Stat = 260
|
|
|
|
Stat_ResistanceBonus_Magic_Thunder Stat = 270
|
|
|
|
Stat_ResistanceBonus_Magic_Acid Stat = 280
|
|
|
|
Stat_ResistanceBonus_Magic_Poison Stat = 290
|
|
|
|
|
2024-06-08 15:33:18 +03:00
|
|
|
Stat_MaxHealthBonus Stat = 1000
|
|
|
|
Stat_SpeedBonus Stat = 1010
|
2024-05-21 23:08:51 +03:00
|
|
|
)
|
|
|
|
|
2024-05-31 23:37:06 +03:00
|
|
|
func StatLongName(stat Stat) string {
|
|
|
|
switch stat {
|
|
|
|
case Stat_Attributes_Strength:
|
|
|
|
return "Strength"
|
|
|
|
case Stat_Attributes_Intelligence:
|
|
|
|
return "Intelligence"
|
|
|
|
case Stat_Attributes_Dexterity:
|
|
|
|
return "Dexterity"
|
|
|
|
case Stat_Attributes_Constitution:
|
|
|
|
return "Constitution"
|
|
|
|
default:
|
|
|
|
return "Unknown"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-01 11:20:51 +03:00
|
|
|
func RandomStats(pointsAvailable int, min, max int, stats []Stat) (result map[Stat]int) {
|
|
|
|
result = map[Stat]int{}
|
|
|
|
|
|
|
|
for {
|
|
|
|
if pointsAvailable == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, s := range stats {
|
|
|
|
if pointsAvailable == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
limit := pointsAvailable
|
|
|
|
|
|
|
|
if limit > max {
|
|
|
|
limit = max
|
|
|
|
}
|
|
|
|
|
|
|
|
value := engine.RandInt(min+1, limit+min)
|
|
|
|
|
|
|
|
result[s] += value
|
|
|
|
|
|
|
|
pointsAvailable -= value - min
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-05-21 23:08:51 +03:00
|
|
|
type StatModifierId string
|
|
|
|
|
|
|
|
type StatModifier struct {
|
|
|
|
Id StatModifierId
|
|
|
|
Stat Stat
|
|
|
|
Bonus int
|
|
|
|
}
|
|
|
|
|
|
|
|
// RPG system is based off of dice rolls
|
|
|
|
|
|
|
|
func rollDice(times, sides int) int {
|
|
|
|
acc := 0
|
|
|
|
|
|
|
|
for range times {
|
|
|
|
acc += 1 + rand.Intn(sides+1)
|
|
|
|
}
|
|
|
|
|
|
|
|
return acc
|
|
|
|
}
|
|
|
|
|
|
|
|
func RollD100(times int) int {
|
|
|
|
return rollDice(times, 100)
|
|
|
|
}
|
|
|
|
|
|
|
|
func RollD20(times int) int {
|
|
|
|
return rollDice(times, 20)
|
|
|
|
}
|
|
|
|
|
|
|
|
func RollD12(times int) int {
|
|
|
|
return rollDice(times, 12)
|
|
|
|
}
|
|
|
|
|
|
|
|
func RollD10(times int) int {
|
|
|
|
return rollDice(times, 10)
|
|
|
|
}
|
|
|
|
|
|
|
|
func RollD8(times int) int {
|
|
|
|
return rollDice(times, 8)
|
|
|
|
}
|
|
|
|
|
|
|
|
func RollD6(times int) int {
|
|
|
|
return rollDice(times, 6)
|
|
|
|
}
|
|
|
|
|
|
|
|
func RollD4(times int) int {
|
|
|
|
return rollDice(times, 4)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Contests are "meets it, beats it"
|
|
|
|
//
|
|
|
|
// Luck roll = 1d10
|
|
|
|
//
|
|
|
|
// 2 rolls per attack:
|
|
|
|
//
|
|
|
|
// BASIC ATTACKS ( spells and abilities can have special rules, or in leu of special rules, these are used ):
|
|
|
|
//
|
|
|
|
// 1. Attack roll ( determines if the attack lands ). Contest between Evasion and Precision.
|
|
|
|
// Evasion = Dexterity + Luck roll.
|
|
|
|
// Precision = ( Strength | Intelligence ) + Luck roll ( intelligence for magic, strength for melee ).
|
|
|
|
//
|
|
|
|
// 2. Damage roll ( only if the previous was successful ). Each spell, ability and weapon has its own damage calculation.
|
|
|
|
|
|
|
|
type DamageType int
|
|
|
|
|
|
|
|
const (
|
|
|
|
DamageType_Physical_Unarmed DamageType = 0
|
|
|
|
DamageType_Physical_Slashing DamageType = 1
|
|
|
|
DamageType_Physical_Piercing DamageType = 2
|
|
|
|
DamageType_Physical_Bludgeoning DamageType = 3
|
|
|
|
|
|
|
|
DamageType_Magic_Fire DamageType = 4
|
|
|
|
DamageType_Magic_Cold DamageType = 5
|
|
|
|
DamageType_Magic_Necrotic DamageType = 6
|
|
|
|
DamageType_Magic_Thunder DamageType = 7
|
|
|
|
DamageType_Magic_Acid DamageType = 8
|
|
|
|
DamageType_Magic_Poison DamageType = 9
|
|
|
|
)
|
|
|
|
|
2024-06-01 11:20:51 +03:00
|
|
|
func DamageTypeName(dmgType DamageType) string {
|
|
|
|
switch dmgType {
|
|
|
|
case DamageType_Physical_Unarmed:
|
|
|
|
return "Unarmed"
|
|
|
|
case DamageType_Physical_Slashing:
|
|
|
|
return "Slashing"
|
|
|
|
case DamageType_Physical_Piercing:
|
|
|
|
return "Piercing"
|
|
|
|
case DamageType_Physical_Bludgeoning:
|
|
|
|
return "Bludgeoning"
|
|
|
|
case DamageType_Magic_Fire:
|
|
|
|
return "Fire"
|
|
|
|
case DamageType_Magic_Cold:
|
|
|
|
return "Cold"
|
|
|
|
case DamageType_Magic_Necrotic:
|
|
|
|
return "Necrotic"
|
|
|
|
case DamageType_Magic_Thunder:
|
|
|
|
return "Thunder"
|
|
|
|
case DamageType_Magic_Acid:
|
|
|
|
return "Acid"
|
|
|
|
case DamageType_Magic_Poison:
|
|
|
|
return "Poison"
|
|
|
|
default:
|
|
|
|
return "Unknown"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-21 23:08:51 +03:00
|
|
|
func DamageTypeToBonusStat(dmgType DamageType) Stat {
|
|
|
|
switch dmgType {
|
|
|
|
case DamageType_Physical_Unarmed:
|
|
|
|
return Stat_DamageBonus_Physical_Unarmed
|
|
|
|
case DamageType_Physical_Slashing:
|
|
|
|
return Stat_DamageBonus_Physical_Slashing
|
|
|
|
case DamageType_Physical_Piercing:
|
|
|
|
return Stat_DamageBonus_Physical_Piercing
|
|
|
|
case DamageType_Physical_Bludgeoning:
|
|
|
|
return Stat_DamageBonus_Physical_Bludgeoning
|
|
|
|
case DamageType_Magic_Fire:
|
|
|
|
return Stat_DamageBonus_Magic_Fire
|
|
|
|
case DamageType_Magic_Cold:
|
2024-06-01 11:20:51 +03:00
|
|
|
return Stat_DamageBonus_Magic_Cold
|
2024-05-21 23:08:51 +03:00
|
|
|
case DamageType_Magic_Necrotic:
|
|
|
|
return Stat_DamageBonus_Magic_Necrotic
|
|
|
|
case DamageType_Magic_Thunder:
|
|
|
|
return Stat_DamageBonus_Magic_Thunder
|
|
|
|
case DamageType_Magic_Acid:
|
|
|
|
return Stat_DamageBonus_Magic_Acid
|
|
|
|
case DamageType_Magic_Poison:
|
|
|
|
return Stat_DamageBonus_Magic_Poison
|
|
|
|
default:
|
|
|
|
return Stat_NonExtant
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:17:22 +03:00
|
|
|
func DamageTypeToResistanceStat(dmgType DamageType) Stat {
|
|
|
|
switch dmgType {
|
|
|
|
case DamageType_Physical_Unarmed:
|
|
|
|
return Stat_ResistanceBonus_Physical_Unarmed
|
|
|
|
case DamageType_Physical_Slashing:
|
|
|
|
return Stat_ResistanceBonus_Physical_Slashing
|
|
|
|
case DamageType_Physical_Piercing:
|
|
|
|
return Stat_ResistanceBonus_Physical_Piercing
|
|
|
|
case DamageType_Physical_Bludgeoning:
|
|
|
|
return Stat_ResistanceBonus_Physical_Bludgeoning
|
|
|
|
case DamageType_Magic_Fire:
|
|
|
|
return Stat_ResistanceBonus_Magic_Fire
|
|
|
|
case DamageType_Magic_Cold:
|
|
|
|
return Stat_ResistanceBonus_Magic_Cold
|
|
|
|
case DamageType_Magic_Necrotic:
|
|
|
|
return Stat_ResistanceBonus_Magic_Necrotic
|
|
|
|
case DamageType_Magic_Thunder:
|
|
|
|
return Stat_ResistanceBonus_Magic_Thunder
|
|
|
|
case DamageType_Magic_Acid:
|
|
|
|
return Stat_ResistanceBonus_Magic_Acid
|
|
|
|
case DamageType_Magic_Poison:
|
|
|
|
return Stat_ResistanceBonus_Magic_Poison
|
|
|
|
default:
|
|
|
|
return Stat_NonExtant
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-21 23:08:51 +03:00
|
|
|
func LuckRoll() int {
|
|
|
|
return RollD10(1)
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:17:22 +03:00
|
|
|
func TotalModifierForStat(stats *Item_StatModifierComponent, stat Stat) int {
|
2024-05-21 23:08:51 +03:00
|
|
|
agg := 0
|
|
|
|
|
2024-06-06 23:17:22 +03:00
|
|
|
for _, m := range stats.StatModifiers {
|
|
|
|
if m.Stat == stat {
|
|
|
|
agg += m.Bonus
|
|
|
|
}
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return agg
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:17:22 +03:00
|
|
|
func statValue(stats *Entity_StatsHolderComponent, stat Stat) int {
|
|
|
|
return stats.BaseStats[stat]
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
|
|
|
|
2024-06-08 15:33:18 +03:00
|
|
|
func statModifierValue(statModifiers []StatModifier, stat Stat) int {
|
|
|
|
for _, sm := range statModifiers {
|
|
|
|
if sm.Stat == stat {
|
|
|
|
return sm.Bonus
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
2024-05-21 23:08:51 +03:00
|
|
|
// Base Max Health is determined from constitution:
|
2024-05-30 23:39:54 +03:00
|
|
|
// 5*Constitution + Max Health Bonus
|
2024-06-06 23:28:06 +03:00
|
|
|
func BaseMaxHealth(entity Entity) int {
|
2024-06-06 23:17:22 +03:00
|
|
|
stats := entity.Stats()
|
|
|
|
|
|
|
|
if stats == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return 5*statValue(stats, Stat_Attributes_Constitution) + statValue(stats, Stat_MaxHealthBonus)
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Dexterity + Evasion bonus + luck roll
|
2024-06-06 23:28:06 +03:00
|
|
|
func EvasionRoll(victim Entity) int {
|
2024-06-06 23:17:22 +03:00
|
|
|
if victim.Stats() == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return statValue(victim.Stats(), Stat_Attributes_Dexterity) + statValue(victim.Stats(), Stat_EvasionBonus) + LuckRoll()
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Strength + Precision bonus ( melee + total ) + luck roll
|
2024-06-06 23:28:06 +03:00
|
|
|
func PhysicalPrecisionRoll(attacker Entity) int {
|
2024-06-06 23:17:22 +03:00
|
|
|
if attacker.Stats() == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return statValue(attacker.Stats(), Stat_Attributes_Strength) + statValue(attacker.Stats(), Stat_PhysicalPrecisionBonus) + statValue(attacker.Stats(), Stat_TotalPrecisionBonus) + LuckRoll()
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Intelligence + Precision bonus ( magic + total ) + luck roll
|
2024-06-06 23:28:06 +03:00
|
|
|
func MagicPrecisionRoll(attacker Entity) int {
|
2024-06-06 23:17:22 +03:00
|
|
|
if attacker.Stats() == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return statValue(attacker.Stats(), Stat_Attributes_Intelligence) + statValue(attacker.Stats(), Stat_MagicPrecisionBonus) + statValue(attacker.Stats(), Stat_TotalPrecisionBonus) + LuckRoll()
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// true = hit lands, false = hit does not land
|
2024-06-06 23:28:06 +03:00
|
|
|
func MagicHitRoll(attacker Entity, victim Entity) bool {
|
2024-05-21 23:08:51 +03:00
|
|
|
return hitRoll(EvasionRoll(victim), MagicPrecisionRoll(attacker))
|
|
|
|
}
|
|
|
|
|
|
|
|
// true = hit lands, false = hit does not land
|
2024-06-06 23:28:06 +03:00
|
|
|
func PhysicalHitRoll(attacker Entity, victim Entity) (hit bool, evasion, precision int) {
|
2024-06-01 11:20:51 +03:00
|
|
|
evasion = EvasionRoll(victim)
|
|
|
|
precision = PhysicalPrecisionRoll(attacker)
|
|
|
|
hit = hitRoll(evasion, precision)
|
|
|
|
|
|
|
|
return
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func hitRoll(evasionRoll, precisionRoll int) bool {
|
|
|
|
return evasionRoll < precisionRoll
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:28:06 +03:00
|
|
|
func UnarmedDamage(attacker Entity) int {
|
2024-06-06 23:17:22 +03:00
|
|
|
if attacker.Stats() == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
return RollD4(1) + statValue(attacker.Stats(), Stat_DamageBonus_Physical_Unarmed)
|
2024-05-21 23:08:51 +03:00
|
|
|
}
|
2024-06-01 11:20:51 +03:00
|
|
|
|
2024-06-06 23:28:06 +03:00
|
|
|
func PhysicalWeaponDamage(attacker Entity, weapon Item, victim Entity) (totalDamage int, dmgType DamageType) {
|
2024-06-06 23:17:22 +03:00
|
|
|
if attacker.Stats() == nil || weapon.Damaging() == nil || victim.Stats() == nil {
|
2024-06-08 15:33:18 +03:00
|
|
|
return UnarmedDamage(attacker), DamageType_Physical_Unarmed
|
2024-06-06 23:17:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
totalDamage, dmgType = weapon.Damaging().DamageRoll()
|
2024-06-01 11:20:51 +03:00
|
|
|
|
|
|
|
bonusDmgStat := DamageTypeToBonusStat(dmgType)
|
2024-06-06 23:17:22 +03:00
|
|
|
dmgResistStat := DamageTypeToResistanceStat(dmgType)
|
2024-06-01 11:20:51 +03:00
|
|
|
|
2024-06-06 23:17:22 +03:00
|
|
|
totalDamage = totalDamage + statValue(attacker.Stats(), bonusDmgStat) - statValue(victim.Stats(), dmgResistStat)
|
2024-06-01 11:20:51 +03:00
|
|
|
|
2024-06-08 15:33:18 +03:00
|
|
|
if weapon.StatModifier() != nil {
|
|
|
|
totalDamage += statModifierValue(weapon.StatModifier().StatModifiers, bonusDmgStat)
|
|
|
|
}
|
|
|
|
|
|
|
|
if totalDamage <= 0 {
|
|
|
|
return 0, dmgType
|
|
|
|
}
|
|
|
|
|
2024-06-01 11:20:51 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:28:06 +03:00
|
|
|
func UnarmedAttack(attacker Entity, victim Entity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
|
2024-06-01 11:20:51 +03:00
|
|
|
hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim)
|
|
|
|
|
|
|
|
if !hit {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
damage = UnarmedDamage(attacker)
|
|
|
|
damageType = DamageType_Physical_Unarmed
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:28:06 +03:00
|
|
|
func PhysicalWeaponAttack(attacker Entity, weapon Item, victim Entity) (hit bool, precisionRoll, evasionRoll int, damage int, damageType DamageType) {
|
2024-06-01 11:20:51 +03:00
|
|
|
hit, evasionRoll, precisionRoll = PhysicalHitRoll(attacker, victim)
|
|
|
|
|
|
|
|
if !hit {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-06 23:17:22 +03:00
|
|
|
damage, damageType = PhysicalWeaponDamage(attacker, weapon, victim)
|
2024-06-01 11:20:51 +03:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|