feat(game): 动物自然生成(草面/白天/限频)与繁殖冷却/成长;村民交易表数据驱动(含测试)
This commit is contained in:
20
assets/config/villager_trades.json
Normal file
20
assets/config/villager_trades.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"professions": {
|
||||
"farmer": [
|
||||
{ "buy": "wheat", "buy_n": 20, "sell": "rotten_flesh", "sell_n": 1 },
|
||||
{ "buy": "wheat", "buy_n": 3, "sell": "beef", "sell_n": 1 }
|
||||
],
|
||||
"builder": [
|
||||
{ "buy": "oak_planks", "buy_n": 16, "sell": "stick", "sell_n": 4 },
|
||||
{ "buy": "cobblestone", "buy_n": 8, "sell": "oak_planks", "sell_n": 4 }
|
||||
],
|
||||
"blacksmith": [
|
||||
{ "buy": "coal", "buy_n": 8, "sell": "iron_ingot", "sell_n": 1 },
|
||||
{ "buy": "iron_ingot", "buy_n": 2, "sell": "diamond", "sell_n": 1 }
|
||||
],
|
||||
"trader": [
|
||||
{ "buy": "rotten_flesh", "buy_n": 8, "sell": "bone", "sell_n": 2 },
|
||||
{ "buy": "feather", "buy_n": 4, "sell": "egg", "sell_n": 1 }
|
||||
]
|
||||
}
|
||||
}
|
||||
131
internal/entity/breeding.go
Normal file
131
internal/entity/breeding.go
Normal 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]
|
||||
}
|
||||
66
internal/entity/breeding_test.go
Normal file
66
internal/entity/breeding_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,11 @@ type Session struct {
|
||||
Entities *entity.Manager
|
||||
BlockEntities map[[3]int32]*blockentity.Entity
|
||||
|
||||
// 自然生成计时(僵尸与敌对生物.md §5:夜晚/黑暗处刷怪)
|
||||
spawnTimer float64
|
||||
damageIn float64 // 玩家受击冷却
|
||||
rng *rand.Rand
|
||||
// 自然生成计时(僵尸与敌对生物.md §5、动物体系.md §6)
|
||||
spawnTimer float64
|
||||
animalTimer float64
|
||||
damageIn float64 // 玩家受击冷却
|
||||
rng *rand.Rand
|
||||
|
||||
// 破坏进度状态(方块交互与动画.md §3)
|
||||
breakTarget raycast.Hit
|
||||
@@ -84,6 +85,8 @@ func (s *Session) Tick(dt float64) {
|
||||
}
|
||||
// 夜间刷僵尸(僵尸与敌对生物.md §5:黑暗处、距玩家 24 格、限频)
|
||||
s.spawnZombies(dt)
|
||||
// 白天刷动物(动物体系.md §6:草面、亮度≥9、距玩家 16–32、限频)
|
||||
s.spawnAnimals(dt)
|
||||
// 僵尸近身伤害(僵尸与敌对生物.md §3:3 点/秒)
|
||||
s.applyZombieDamage(dt)
|
||||
// 相机跟随玩家眼睛
|
||||
@@ -278,6 +281,49 @@ func (s *Session) spawnZombies(dt float64) {
|
||||
}
|
||||
}
|
||||
|
||||
// spawnAnimals 白天刷动物(动物体系.md §6:草面、天空光 ≥9、距玩家 16–32、每 10 秒一只)。
|
||||
func (s *Session) spawnAnimals(dt float64) {
|
||||
s.animalTimer -= dt
|
||||
if s.animalTimer > 0 {
|
||||
return
|
||||
}
|
||||
s.animalTimer = 10.0
|
||||
for i := 0; i < 16; i++ {
|
||||
ang := s.rng.Float64() * 2 * math.Pi
|
||||
dist := 16 + s.rng.Float64()*16
|
||||
x := int32(s.Player.Pos[0] + math.Cos(ang)*dist)
|
||||
z := int32(s.Player.Pos[2] + math.Sin(ang)*dist)
|
||||
var y int32
|
||||
for y = 255; y > 0; y-- {
|
||||
if s.World.Block(x, y, z) != block.Air {
|
||||
break
|
||||
}
|
||||
}
|
||||
if s.World.Block(x, y+1, z) != block.Air {
|
||||
continue
|
||||
}
|
||||
// 只在草面上且白天(天空光 ≥9)
|
||||
if s.World.Block(x, y, z).ID() != s.grassID() {
|
||||
continue
|
||||
}
|
||||
if s.World.SkyLight(x, y+1, z) < 9 {
|
||||
continue
|
||||
}
|
||||
e := s.Entities.Spawn(entity.KindAnimal, float64(x)+0.5, float64(y+1), float64(z)+0.5)
|
||||
e.Data = &entity.Animal{}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// grassID 草方块 ID(注册表查询缓存值)。
|
||||
func (s *Session) grassID() uint16 {
|
||||
id, ok := s.Reg.ID("grass_block")
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// applyZombieDamage 僵尸近身伤害(僵尸与敌对生物.md §3:约 3 点/秒)。
|
||||
func (s *Session) applyZombieDamage(dt float64) {
|
||||
if s.Health <= 0 {
|
||||
|
||||
12
文档/构建与发布.md
12
文档/构建与发布.md
@@ -75,11 +75,13 @@ mc-v1.2.0/
|
||||
|
||||
## 8. Windows 安装包(年糕历险记 · 中文安装)
|
||||
|
||||
- 产品名称:**年糕历险记**(安装包文件名 `年糕历险记-Setup-v1.0.0.exe`;产品、快捷方式、卸载项同名)。
|
||||
- 安装器:NSIS(中文安装界面),`scripts/installer.nsi`(含 简体中文 语言、安装目录选择、桌面/开始菜单快捷方式、卸载器)。
|
||||
- 安装内容:`mc-client.exe`、`assets/`(未嵌入时)、`config/`、`mods/`(空目录 + README)、`resourcepacks/`(默认素材包)、`worlds/`。
|
||||
- 中文要求:安装器文案全中文;游戏内 `lang/zh_cn.json` 随安装包提供;缺失翻译键回退 en_us。
|
||||
- 安装包构建仅在**发布时**执行(M6,构建与发布.md §6 发布清单第 10 项),开发期无需 NSIS。
|
||||
- 产品名称:**年糕历险记**(安装包文件名 `年糕历险记-Setup-v<版本>`;产品、快捷方式、卸载项同名)。
|
||||
- **两条交付路径**:
|
||||
1. **便携安装包(当前交付)**:`pwsh scripts/make_installer.ps1` 生成 `dist/年糕历险记-Setup-v<版本>.zip`,解压后运行 `安装.bat`——全中文交互(选择安装目录、复制文件、创建桌面快捷方式「年糕历险记」)。
|
||||
2. **NSIS 正式安装器(发布时)**:`scripts/installer.nsi`(简体中文界面、开始菜单、卸载器),需发布环境安装 NSIS 3:`makensis /DVERSION=<版本> scripts/installer.nsi`。
|
||||
- 安装内容:`mc-client.exe`、`mc-server.exe`、`config/`、`mods/`(README)、`resourcepacks/`(默认素材包)。
|
||||
- 中文要求:安装脚本全中文;游戏内 `lang/zh_cn.json` 随安装包提供;缺失翻译键回退 en_us。
|
||||
- 存档位置:安装目录 `worlds/`(便携式;NSIS 版卸载时保留存档)。
|
||||
|
||||
## 9. CI 发布(GitHub Actions 草案)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user