feat: entity definitions — player classes, monsters, items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 23:44:56 +09:00
parent d36e364491
commit e7b12bae08
5 changed files with 293 additions and 0 deletions

36
entity/item.go Normal file
View 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
View 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
View 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
View 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
View 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")
}
}