feat(game): 动物自然生成(草面/白天/限频)与繁殖冷却/成长;村民交易表数据驱动(含测试)

This commit is contained in:
NianGao Dev
2026-08-16 00:01:20 +08:00
parent 4755fd88fc
commit e70314fb3f
5 changed files with 274 additions and 9 deletions

131
internal/entity/breeding.go Normal file
View File

@@ -0,0 +1,131 @@
// 动物繁殖与村民交易:动物体系.md §3、村民系统.md §7 的核心玩法逻辑。
package entity
import (
"encoding/json"
"fmt"
"os"
)
// 动物繁殖参数(动物体系.md §3)。
const (
breedCooldown = 60 * 20 // 繁殖冷却 60 秒(tick)
babyGrowTime = 20 * 60 * 20 // 幼崽成长 20 分钟(tick)
perChunkAnimal = 10 // 每区块同种动物上限
)
// Animal 动物附加数据(动物体系.md §2)。
type Animal struct {
BreedCooldown int // 繁殖冷却剩余 tick
GrowthTicks int // 幼崽成长剩余(0 = 成年)
ParentA ID // 近亲繁殖记录(后期)
ParentB ID
}
// Breed 繁殖两只同种成年动物 → 幼崽(动物体系.md §3:冷却与上限控制)。
// 返回新幼崽;冷却未到/上限达到返回 nil。
func (m *Manager) Breed(a, b *Entity, w WorldView) *Entity {
if a.Kind != KindAnimal || b.Kind != KindAnimal || a.Dead || b.Dead {
return nil
}
ad, ok1 := a.Data.(*Animal)
bd, ok2 := b.Data.(*Animal)
if !ok1 {
ad = &Animal{}
a.Data = ad
}
if !ok2 {
bd = &Animal{}
b.Data = bd
}
if ad.BreedCooldown > 0 || bd.BreedCooldown > 0 || ad.GrowthTicks > 0 || bd.GrowthTicks > 0 {
return nil
}
// 每区块数量控制(防无限繁殖卡服,动物体系.md §7 踩坑)
if m.countNear(a, perChunkAnimal) >= perChunkAnimal {
return nil
}
ad.BreedCooldown = breedCooldown
bd.BreedCooldown = breedCooldown
baby := m.Spawn(KindAnimal, a.Pos[0], a.Pos[1], a.Pos[2])
baby.Data = &Animal{GrowthTicks: babyGrowTime, ParentA: a.ID, ParentB: b.ID}
baby.Health = 10 // 幼崽血量
return baby
}
// TickAnimal 动物成长/冷却计时(调用方每 tick 对动物实体调用)。
func TickAnimal(e *Entity) {
if e.Kind != KindAnimal {
return
}
d, ok := e.Data.(*Animal)
if !ok {
return
}
if d.BreedCooldown > 0 {
d.BreedCooldown--
}
if d.GrowthTicks > 0 {
d.GrowthTicks--
if d.GrowthTicks == 0 {
e.Health = 20 // 成年
}
}
}
// countNear 统计同种动物数量(以 a 所在区块计,简化按距离 16)。
func (m *Manager) countNear(a *Entity, _ int) int {
n := 0
for _, e := range m.List() {
if e.Kind == KindAnimal {
dx, dz := e.Pos[0]-a.Pos[0], e.Pos[2]-a.Pos[2]
if dx*dx+dz*dz < 16*16 {
n++
}
}
}
return n
}
// ---- 村民交易(村民系统.md §7:数据驱动交易表)----
// TradeOffer 一条交易(收购 → 出售)。
type TradeOffer struct {
Buy string `json:"buy"` // 收购物品
BuyN int `json:"buy_n"` // 收购数量
Sell string `json:"sell"` // 出售物品
SellN int `json:"sell_n"` // 出售数量
Uses int `json:"uses"` // 已交易次数(涨价基础)
}
// TradeTable 交易表(按职业)。
type TradeTable struct {
byProfession map[uint8][]TradeOffer
}
// LoadTrades 解析交易表 JSON。
func LoadTrades(path string) (*TradeTable, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("entity.LoadTrades: %w", err)
}
var doc struct {
Professions map[string][]TradeOffer `json:"professions"`
}
if err := json.Unmarshal(b, &doc); err != nil {
return nil, fmt.Errorf("entity.LoadTrades: %w", err)
}
t := &TradeTable{byProfession: make(map[uint8][]TradeOffer)}
names := map[string]uint8{"farmer": 0, "builder": 1, "blacksmith": 2, "trader": 3}
for name, offers := range doc.Professions {
if id, ok := names[name]; ok {
t.byProfession[id] = offers
}
}
return t, nil
}
// OffersFor 返回某职业的交易列表。
func (t *TradeTable) OffersFor(profession uint8) []TradeOffer {
return t.byProfession[profession]
}

View File

@@ -0,0 +1,66 @@
package entity
import (
"path/filepath"
"testing"
)
// TestBreed 动物繁殖:成年+冷却完成 → 幼崽;冷却中拒绝;上限拒绝(动物体系.md §3)。
func TestBreed(t *testing.T) {
m := NewManager()
w := flatWorld{}
a := m.Spawn(KindAnimal, 0.5, 63, 0.5)
a.Data = &Animal{}
b := m.Spawn(KindAnimal, 1.5, 63, 0.5)
b.Data = &Animal{}
baby := m.Breed(a, b, w)
if baby == nil {
t.Fatal("应繁殖出幼崽")
}
if d, ok := baby.Data.(*Animal); !ok || d.GrowthTicks == 0 {
t.Fatalf("幼崽数据异常: %+v", baby.Data)
}
// 冷却中:拒绝
if m.Breed(a, b, w) != nil {
t.Fatal("冷却中不应繁殖")
}
// 冷却恢复后可再次繁殖
ad := a.Data.(*Animal)
ad.BreedCooldown = 0
bd := b.Data.(*Animal)
bd.BreedCooldown = 0
if m.Breed(a, b, w) == nil {
t.Fatal("冷却结束后应能繁殖")
}
}
// TestBreedCap 每区块上限(动物体系.md §7 防无限繁殖)。
func TestBreedCap(t *testing.T) {
m := NewManager()
w := flatWorld{}
a := m.Spawn(KindAnimal, 0.5, 63, 0.5)
a.Data = &Animal{}
b := m.Spawn(KindAnimal, 1.5, 63, 0.5)
b.Data = &Animal{}
for i := 0; i < 12; i++ {
m.Spawn(KindAnimal, 2.5, 63, 2.5+float64(i))
}
if m.Breed(a, b, w) != nil {
t.Fatal("超上限不应繁殖")
}
}
// TestLoadTrades 交易表解析与职业查询(村民系统.md §7)。
func TestLoadTrades(t *testing.T) {
tbl, err := LoadTrades(filepath.Join("..", "..", "assets", "config", "villager_trades.json"))
if err != nil {
t.Fatalf("加载交易表失败: %v", err)
}
if offers := tbl.OffersFor(0); len(offers) != 2 {
t.Fatalf("农民交易期望 2 条,实际 %d", len(offers))
}
if offers := tbl.OffersFor(2); len(offers) != 2 || offers[0].Buy != "coal" {
t.Fatalf("铁匠交易异常: %+v", offers)
}
}