Merchant(商人)
EntityMerchant—— 本 mod 唯一注册的实体。放置 Market 方块 时生成,全 mod 全部市场状态都存在这个实体上。
基本信息
| 属性 | 值 | 来源 |
|---|---|---|
| 类 | com.guigs44.farmingforengineers.entity.EntityMerchant(extends EntityCreature implements INpc) |
entity/EntityMerchant.java:28 |
| 注册名 | farmingforengineers:merchant |
FarmingForEngineers.java:89 |
| 实体 ID | 0 |
FarmingForEngineers.java:89 |
| 追踪范围 / 更新频率 | 64 / 3 tick |
FarmingForEngineers.java:89 |
| 生命值 | 20(applyEntityAttributes 只设了 movementSpeed,未设 maxHealth,沿用 EntityCreature 默认 20) |
entity/EntityMerchant.java:74 |
| 护甲 | 0(armor 属性未设置) |
— |
| 碰撞箱 | 0.6 × 1.95(setSize(0.6f, 1.95f)) |
entity/EntityMerchant.java:58 |
| 眼高 | 1.62F |
entity/EntityMerchant.java:210 |
| 移动速度 | 0.5(SharedMonsterAttributes.movementSpeed 基值) |
entity/EntityMerchant.java:77 |
| 渲染 | RenderMerchant:直接复用 ModelVillager(shadow 0.5),贴图 farmingforengineers:textures/entity/merchant.png |
client/render/RenderMerchant.java:20 |
isAIEnabled |
true |
entity/EntityMerchant.java:64 |
| 攻击方式 | 无(未覆写 attackEntityAsMob / attackEntityAt) |
— |
生成
- 不自然生成:
EntityRegistry.registerModEntity(..., mod = this, 64, 3, true)的最后一位是sendVelocityUpdates = true,不是世界生成。LivingHandler/EntityLiving实体生成器在src/main/java/全仓库零命中。 - 唯一来源:玩家放置市场方块时由
BlockMarket.onBlockPlacedBy在方块正对面手动world.spawnEntityInWorld(merchant)。 - 生成群系 / 高度:不适用(唯一来源是方块位置)。
行为(AI 任务表)
EntityMerchant 构造函数只注册 3 个任务(entity/EntityMerchant.java:59):
| 优先级 | 任务 | 说明 |
|---|---|---|
| 0 | EntityAISwimming |
原版,避免淹死 |
| 1 | EntityAIAvoidEntity(this, EntityZombie.class, 8f, 0.6, 0.6) |
距僵尸 8 格内绕开 |
| 5 | EntityAIMerchant(this, 0.6) |
走回自己的市场方块,速度 0.6 |
EntityAIMerchant 的逻辑:
shouldExecute():已在市场(isAtMarket())则调setToFacingAngle()并返回false;否则把目标点设为市场 +facing.getOpposite()偏移,返回truecontinueExecuting():!entity.getNavigator().noPath()startExecuting():navigator.tryMoveToXYZ(movePosX, movePosY, movePosZ, 0.6)setMutexBits(1)
isAtMarket() 判据为距离平方 ≤ MARKET_ARRIVAL_DISTANCE = 1.75 × 1.75。
[!WARNING]
EntityAIMerchant.java:30的 Z 分量写的是opposite.offsetY而不是offsetZ:movePosZ = entity.marketZ + opposite.offsetY。因为只取 X/Z 水平朝向,offsetY恒为 0,这行等价于marketZ + 0,即导航目标永远落在市场的同一 Z 平面上,不做前后偏移。属于源码既有行为,非笔误推断。
生命周期:脱离市场即消失
onEntityUpdate(entity/EntityMerchant.java:144)在服务端每 20 tick 检查一次:
isMarketValid()为false→worldObj.setEntityState(this, (byte) 12)然后setDead();客户端收到状态 12 时执行disappear()(播fire.ignite+ 50 个fireworksSpark+ 1 个largeexplode)isMarketValid()的判定:worldObj.getBlock(marketX, marketY, marketZ) == ModBlocks.market || == Blocks.bedrock。源码注释说明加载世界初期getBlock会短暂返回基岩,故把基岩也算作「有效」,避免每次读档商人集体消失。canDespawn()返回false,商人永不自然消失——只会因为市场方块被拆而消失。
出生动画变体(SpawnAnimationType,3 值)
由 BlockMarket.onBlockPlacedBy 依环境挑选,每个变体的行为差异如下:
| 枚举值 | 触发条件 | 位置 | 服务端行为 | 客户端表现 |
|---|---|---|---|---|
FALLING |
市场方块处 canBlockSeeTheSky |
merchantY + 172 |
写入 NBT SpawnAnimation = 1 |
播放 farmingforengineers:falling 音效;从 172 格高自由落体;spawnDone 仍为 false |
DIGGING |
不见天,且市场方块正下方 !isAirBlock(x, y-1, z) |
merchantY + 0.5 或 merchantY |
下一 tick 发状态 13 并置 spawnDone = true |
收到 13 → diggingAnimation = 60;每 tick 递减并每 2 tick 播一次挖掘音 Block.soundTypeGravel.getBreakSound()(音量 1.0,音调 random() + 0.5);每 tick 尝试生成方块碎屑粒子(已注释掉,见下方「死代码」) |
MAGIC |
以上都不成立 | merchantY + 0.5 或 merchantY |
无额外状态 | 立即播 fire.ignite(ModSounds.LOC_POOF)并爆 50 个 fireworksSpark + 1 个 largeexplode |
FALLING 变体有一处专门的免伤处理(entity/EntityMerchant.java:200):
若
!spawnDone && damageSrc == DamageSource.fall,播放getHurtSound()(mob.villager.hit,音量 1.0 音调 2.0),把spawnDone置true并return——从天上摔下来的第一次坠落伤害被完全吞掉,不会真的摔死。
[!NOTE]
MAGIC与FALLING在onEntityUpdate里都不会自动置spawnDone = true(只有DIGGING走状态 13 那条路)。FALLING靠吞掉第一次坠落伤害的副作用顺带置位。
名称
NAMES 是 4 个字符串,onInitialSpawn 随机取一个写入 CustomNameTag,并 setAlwaysRenderNameTag(true):
| 名称 |
|---|
Swap-O-Matic |
Emerald Muncher |
Back Alley Dealer |
Weathered Salesperson |
另有 0.1% 概率(Math.random() < 0.001)改名为 Pam 或 Blay(各 50%)。
NBT 字段
writeEntityToNBT / readEntityFromNBT 写 4 个自定义键(entity/EntityMerchant.java:96):
| 键 | 类型 | 含义 |
|---|---|---|
MarketPosX |
int | 市场方块 X |
MarketPosY |
int | 市场方块 Y |
MarketPosZ |
int | 市场方块 Z |
SpawnDone |
boolean | 出生动画是否已完成(影响免伤与 DIGGING 后续) |
SpawnAnimation |
byte | SpawnAnimationType.ordinal()(0 MAGIC / 1 FALLING / 2 DIGGING) |
[!WARNING] 读档时存在两处源码既有缺陷,读出来的市场坐标与朝向都是错的:
readEntityFromNBT判if (compound.hasKey("MarketPosX"))才重建市场,但调用的是setMarket(marketX, marketY, marketZ, ...),传的是尚未赋值的字段本身(此刻全是0),而不是从 compound 取值。- 紧接着读
ForgeDirection.getOrientation(compound.getByte("Facing")),但Facing这个键从未被写入(源码里compound.setByte("Facing", ...)被注释掉了,见entity/EntityMerchant.java:103)。getByte对不存在的键返回0→ForgeDirection.DOWN。净效果:读档后商人的市场坐标归 (0,0,0)、朝向朝下。
isMarketValid()会读到原版世界原点(通常是石头而非基岩)→ 判为无效 → 20 tick 后商人消失。玩家需要重新放一个市场方块。spawnDone与SpawnAnimation两条不受影响,读档正常。
未写入的还有 facing(注释掉的第 6 个键)与 diggingAnimation(纯客户端状态,客户端自己从状态 13 重建)。
掉落
无掉落。未覆写 dropFewItems / dropRareDrop / dropXp(grep 三个模式全仓库 0 命中),也没有 onDeath 里的 entityDropItem——杀死商人只有经验球。
音效
三个声音全部复用原版,无一个是 mod 自有实体音效:
| 用途 | 声音 |
|---|---|
闲置 getLivingSound() |
mob.villager.idle |
受伤 getHurtSound() |
mob.villager.hit |
死亡 getDeathSound() |
mob.villager.death |
交互
| 交互方 | 行为 |
|---|---|
| 玩家右键 | interact() → 仅当 isMarketValid() 为 true 时 player.openGui(MOD_ID, GuiHandler.MARKET, world, marketX, marketY, marketZ);否则 return super.interact(player)(对普通村民 AI 实体等同无操作) |
| 拴绳 | 未覆写 canBeLeashedTo(覆写被注释掉)→ 走 EntityCreature 默认,可被拴 |
| 活塞 | 未做处理,随活塞推动(推动后仍会按 20 tick 校验并回走) |
死代码(源码内被注释 / 空转)
onEntityUpdate里DIGGING阶段的 4 次/ tick 粒子循环:循环体只做int stateId = 0;,所有spawnParticle调用均被注释 → 实际不产生任何粒子EntityAIMerchant的movePosY赋值后tryMoveToXYZ会用到,但目标 Y 恒等于市场 YgetLivingSound等三方法在INpc接口下不被调用路径使用,仅防御性实现
相关条目
- Market 方块 - 生成源;坐标语义(metadata 朝向)与商人
facing一致 - 市场注册表 Market.json - 商人交互后打开的 GUI 售卖内容