feat: entity definitions — player classes, monsters, items
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
36
entity/item.go
Normal file
36
entity/item.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package entity
|
||||
|
||||
type ItemType int
|
||||
|
||||
const (
|
||||
ItemWeapon ItemType = iota
|
||||
ItemArmor
|
||||
ItemConsumable
|
||||
)
|
||||
|
||||
type Item struct {
|
||||
Name string
|
||||
Type ItemType
|
||||
Bonus int
|
||||
Price int
|
||||
}
|
||||
|
||||
type RelicEffect int
|
||||
|
||||
const (
|
||||
RelicHealOnKill RelicEffect = iota
|
||||
RelicATKBoost
|
||||
RelicDEFBoost
|
||||
RelicGoldBoost
|
||||
)
|
||||
|
||||
type Relic struct {
|
||||
Name string
|
||||
Effect RelicEffect
|
||||
Value int
|
||||
Price int
|
||||
}
|
||||
|
||||
func NewHPPotion() Item {
|
||||
return Item{Name: "HP Potion", Type: ItemConsumable, Bonus: 30, Price: 20}
|
||||
}
|
||||
83
entity/monster.go
Normal file
83
entity/monster.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package entity
|
||||
|
||||
import "math"
|
||||
|
||||
type MonsterType int
|
||||
|
||||
const (
|
||||
MonsterSlime MonsterType = iota
|
||||
MonsterSkeleton
|
||||
MonsterOrc
|
||||
MonsterDarkKnight
|
||||
MonsterBoss5
|
||||
MonsterBoss10
|
||||
MonsterBoss15
|
||||
MonsterBoss20
|
||||
)
|
||||
|
||||
type monsterBase struct {
|
||||
Name string
|
||||
HP, ATK, DEF int
|
||||
MinFloor int
|
||||
IsBoss bool
|
||||
}
|
||||
|
||||
var monsterDefs = map[MonsterType]monsterBase{
|
||||
MonsterSlime: {"Slime", 20, 5, 1, 1, false},
|
||||
MonsterSkeleton: {"Skeleton", 35, 10, 4, 3, false},
|
||||
MonsterOrc: {"Orc", 55, 14, 6, 6, false},
|
||||
MonsterDarkKnight: {"Dark Knight", 80, 18, 10, 12, false},
|
||||
MonsterBoss5: {"Guardian", 150, 15, 8, 5, true},
|
||||
MonsterBoss10: {"Warden", 250, 22, 12, 10, true},
|
||||
MonsterBoss15: {"Overlord", 400, 30, 16, 15, true},
|
||||
MonsterBoss20: {"Archlich", 600, 40, 20, 20, true},
|
||||
}
|
||||
|
||||
type Monster struct {
|
||||
Name string
|
||||
Type MonsterType
|
||||
HP, MaxHP int
|
||||
ATK, DEF int
|
||||
IsBoss bool
|
||||
TauntTarget bool
|
||||
TauntTurns int
|
||||
}
|
||||
|
||||
func NewMonster(mt MonsterType, floor int) *Monster {
|
||||
base := monsterDefs[mt]
|
||||
scale := 1.0
|
||||
if !base.IsBoss && floor > base.MinFloor {
|
||||
scale = math.Pow(1.15, float64(floor-base.MinFloor))
|
||||
}
|
||||
hp := int(math.Round(float64(base.HP) * scale))
|
||||
atk := int(math.Round(float64(base.ATK) * scale))
|
||||
return &Monster{
|
||||
Name: base.Name,
|
||||
Type: mt,
|
||||
HP: hp,
|
||||
MaxHP: hp,
|
||||
ATK: atk,
|
||||
DEF: base.DEF,
|
||||
IsBoss: base.IsBoss,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monster) TakeDamage(dmg int) {
|
||||
m.HP -= dmg
|
||||
if m.HP < 0 {
|
||||
m.HP = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monster) IsDead() bool {
|
||||
return m.HP <= 0
|
||||
}
|
||||
|
||||
func (m *Monster) TickTaunt() {
|
||||
if m.TauntTurns > 0 {
|
||||
m.TauntTurns--
|
||||
if m.TauntTurns == 0 {
|
||||
m.TauntTarget = false
|
||||
}
|
||||
}
|
||||
}
|
||||
25
entity/monster_test.go
Normal file
25
entity/monster_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"math"
|
||||
)
|
||||
|
||||
func TestMonsterScaling(t *testing.T) {
|
||||
slime := NewMonster(MonsterSlime, 1)
|
||||
if slime.HP != 20 || slime.ATK != 5 {
|
||||
t.Errorf("Slime floor 1: got HP=%d ATK=%d, want HP=20 ATK=5", slime.HP, slime.ATK)
|
||||
}
|
||||
slimeF3 := NewMonster(MonsterSlime, 3)
|
||||
expectedHP := int(math.Round(20 * math.Pow(1.15, 2)))
|
||||
if slimeF3.HP != expectedHP {
|
||||
t.Errorf("Slime floor 3: got HP=%d, want %d", slimeF3.HP, expectedHP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBossStats(t *testing.T) {
|
||||
boss := NewMonster(MonsterBoss5, 5)
|
||||
if boss.HP != 150 || boss.ATK != 15 || boss.DEF != 8 {
|
||||
t.Errorf("Boss5: got HP=%d ATK=%d DEF=%d, want 150/15/8", boss.HP, boss.ATK, boss.DEF)
|
||||
}
|
||||
}
|
||||
96
entity/player.go
Normal file
96
entity/player.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package entity
|
||||
|
||||
type Class int
|
||||
|
||||
const (
|
||||
ClassWarrior Class = iota
|
||||
ClassMage
|
||||
ClassHealer
|
||||
ClassRogue
|
||||
)
|
||||
|
||||
func (c Class) String() string {
|
||||
return [...]string{"Warrior", "Mage", "Healer", "Rogue"}[c]
|
||||
}
|
||||
|
||||
type classStats struct {
|
||||
HP, ATK, DEF int
|
||||
}
|
||||
|
||||
var classBaseStats = map[Class]classStats{
|
||||
ClassWarrior: {120, 12, 8},
|
||||
ClassMage: {70, 20, 3},
|
||||
ClassHealer: {90, 8, 5},
|
||||
ClassRogue: {85, 15, 4},
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
Name string
|
||||
Fingerprint string
|
||||
Class Class
|
||||
HP, MaxHP int
|
||||
ATK, DEF int
|
||||
Gold int
|
||||
Inventory []Item
|
||||
Relics []Relic
|
||||
Dead bool
|
||||
}
|
||||
|
||||
func NewPlayer(name string, class Class) *Player {
|
||||
stats := classBaseStats[class]
|
||||
return &Player{
|
||||
Name: name,
|
||||
Class: class,
|
||||
HP: stats.HP,
|
||||
MaxHP: stats.HP,
|
||||
ATK: stats.ATK,
|
||||
DEF: stats.DEF,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) TakeDamage(dmg int) {
|
||||
p.HP -= dmg
|
||||
if p.HP <= 0 {
|
||||
p.HP = 0
|
||||
p.Dead = true
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) Heal(amount int) {
|
||||
p.HP += amount
|
||||
if p.HP > p.MaxHP {
|
||||
p.HP = p.MaxHP
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) IsDead() bool {
|
||||
return p.Dead
|
||||
}
|
||||
|
||||
func (p *Player) Revive(hpPercent float64) {
|
||||
p.Dead = false
|
||||
p.HP = int(float64(p.MaxHP) * hpPercent)
|
||||
if p.HP < 1 {
|
||||
p.HP = 1
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) EffectiveATK() int {
|
||||
atk := p.ATK
|
||||
for _, item := range p.Inventory {
|
||||
if item.Type == ItemWeapon {
|
||||
atk += item.Bonus
|
||||
}
|
||||
}
|
||||
return atk
|
||||
}
|
||||
|
||||
func (p *Player) EffectiveDEF() int {
|
||||
def := p.DEF
|
||||
for _, item := range p.Inventory {
|
||||
if item.Type == ItemArmor {
|
||||
def += item.Bonus
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
53
entity/player_test.go
Normal file
53
entity/player_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package entity
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewPlayer(t *testing.T) {
|
||||
p := NewPlayer("testuser", ClassWarrior)
|
||||
if p.HP != 120 || p.MaxHP != 120 {
|
||||
t.Errorf("Warrior HP: got %d, want 120", p.HP)
|
||||
}
|
||||
if p.ATK != 12 {
|
||||
t.Errorf("Warrior ATK: got %d, want 12", p.ATK)
|
||||
}
|
||||
if p.DEF != 8 {
|
||||
t.Errorf("Warrior DEF: got %d, want 8", p.DEF)
|
||||
}
|
||||
if p.Gold != 0 {
|
||||
t.Errorf("Initial gold: got %d, want 0", p.Gold)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllClasses(t *testing.T) {
|
||||
tests := []struct {
|
||||
class Class
|
||||
hp, atk, def int
|
||||
}{
|
||||
{ClassWarrior, 120, 12, 8},
|
||||
{ClassMage, 70, 20, 3},
|
||||
{ClassHealer, 90, 8, 5},
|
||||
{ClassRogue, 85, 15, 4},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
p := NewPlayer("test", tt.class)
|
||||
if p.HP != tt.hp || p.ATK != tt.atk || p.DEF != tt.def {
|
||||
t.Errorf("Class %v: got HP=%d ATK=%d DEF=%d, want HP=%d ATK=%d DEF=%d",
|
||||
tt.class, p.HP, p.ATK, p.DEF, tt.hp, tt.atk, tt.def)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlayerTakeDamage(t *testing.T) {
|
||||
p := NewPlayer("test", ClassWarrior)
|
||||
p.TakeDamage(30)
|
||||
if p.HP != 90 {
|
||||
t.Errorf("HP after 30 dmg: got %d, want 90", p.HP)
|
||||
}
|
||||
p.TakeDamage(200)
|
||||
if p.HP != 0 {
|
||||
t.Errorf("HP should not go below 0: got %d", p.HP)
|
||||
}
|
||||
if !p.IsDead() {
|
||||
t.Error("Player should be dead")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user