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