From 2cc87fc08c7c9824c33b4779d6c894eed819a1de Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 19:47:04 +0200 Subject: [PATCH] feat: extend classes with Classes.isValid and Classes.apply --- src/classes.lua | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ src/combat.lua | 45 +++++++++++++++++++++++++++------- 2 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 src/classes.lua diff --git a/src/classes.lua b/src/classes.lua new file mode 100644 index 0000000..7b8bf43 --- /dev/null +++ b/src/classes.lua @@ -0,0 +1,64 @@ +-- Player classes. A class is chosen once at character creation (Game.new +-- options.class) and never changes; it grants a one-time stat or kit delta +-- and, through skills.lua, gates which skills the player is allowed to spend +-- points on. Classless play (no options.class given) is unaffected - every +-- class delta is opt-in so existing unclassed games keep their exact stats. + +local Items = require("items") + +local Classes = {} + +Classes.order = { "fighter", "rogue", "mage", "ranger" } + +Classes.defs = { + fighter = { + name = "fighter", + apply = function(player) + player.maxHp = player.maxHp + 8 + player.hp = player.hp + 8 + player.attack = player.attack + 2 + player.baseAttack = player.baseAttack + 2 + end, + }, + rogue = { + name = "rogue", + apply = function(player) + player.gold = player.gold + 15 + player.attack = player.attack + 1 + player.baseAttack = player.baseAttack + 1 + end, + }, + mage = { + name = "mage", + apply = function(player) + player.maxMana = player.maxMana + 8 + player.mana = player.mana + 8 + player.attack = player.attack - 1 + player.baseAttack = player.baseAttack - 1 + end, + }, + ranger = { + name = "ranger", + apply = function(player) + Items.equipBow(Items.bow("shortbow", 3, 6), player) + player.inventory[#player.inventory + 1] = Items.ammo("arrow", 12) + end, + }, +} + +function Classes.isValid(key) + return Classes.defs[key] ~= nil +end + +-- Applies the one-time delta for `key` to a freshly created player. Must run +-- after the player's base stats, mana pool and starting inventory are all in +-- place, since every delta is additive on top of whatever is already there. +function Classes.apply(player, key) + local def = Classes.defs[key] + if not def then return false end + player.class = key + def.apply(player) + return true +end + +return Classes diff --git a/src/combat.lua b/src/combat.lua index beca537..b0cc827 100644 --- a/src/combat.lua +++ b/src/combat.lua @@ -1,24 +1,53 @@ -- Deterministic combat: damage is effective attack minus effective defense, --- floored at 1. Effective stats fold in temporary status modifiers. +-- floored at 1. Effective stats fold in temporary status modifiers, plus +-- conditional skill bonuses (berserk/mana shield - see skills.lua, a no-op +-- for any entity without a `skills` table, i.e. monsters) and any completed +-- runeword on the wielder's own weapon/armor (see runewords.lua, a no-op for +-- any item without a matching rune chain), plus any item-set threshold the +-- wielder's current gear happens to meet (see sets.lua, a no-op below its +-- lowest 2-piece threshold). +-- The attack-minus-defense minimum-1 floor happens before elemental +-- resistance (resistances.lua) gets a say, so a defender immune to the +-- attack's damage type can still shrug off a hit that combat itself +-- guaranteed would land. + +local Skills = require("skills") +local Resistances = require("resistances") +local Runewords = require("runewords") +local Sets = require("sets") local Combat = {} local function attackOf(entity) - return entity.effectiveAttack and entity:effectiveAttack() or entity.attack + local base = entity.effectiveAttack and entity:effectiveAttack() or entity.attack + return base + Skills.attackBonus(entity) + Runewords.attackBonus(entity) + Sets.attackBonus(entity) end local function defenseOf(entity) - return entity.effectiveDefense and entity:effectiveDefense() or entity.defense + local base = entity.effectiveDefense and entity:effectiveDefense() or entity.defense + return base + Skills.defenseBonus(entity) + Runewords.defenseBonus(entity) + Sets.defenseBonus(entity) end -function Combat.damage(attacker, defender) - local dealt = attackOf(attacker) - defenseOf(defender) +-- The type of an attacker's raw damage: a weapon retagged by an elemental +-- affix (flaming/venomed - see affixes.lua) fights with that element instead +-- of a bare fist or claw's plain physical. +local function damageTypeOf(attacker) + return (attacker.weapon and attacker.weapon.damageType) or Resistances.PHYSICAL +end + +-- `multiplier` scales the raw attack-minus-defense difference before the +-- floor-at-1 minimum is applied, so a backstab still guarantees at least 1 +-- rather than 2x-ing an already-clamped hit. `damageType` defaults to the +-- attacker's own weapon typing when omitted. +function Combat.damage(attacker, defender, multiplier, damageType) + local dealt = (attackOf(attacker) - defenseOf(defender)) * (multiplier or 1) if dealt < 1 then dealt = 1 end - return dealt + dealt = math.floor(dealt) + return Resistances.apply(defender, dealt, damageType or damageTypeOf(attacker)) end -function Combat.attack(attacker, defender) - local dealt = Combat.damage(attacker, defender) +function Combat.attack(attacker, defender, multiplier, damageType) + local dealt = Combat.damage(attacker, defender, multiplier, damageType) defender.hp = defender.hp - dealt return { damage = dealt, killed = defender.hp <= 0 } end