XFEstudio/MCIndustry / scripts/blocks/磁铜传送带.js

// StackConveyor 的标称速度是“整组容量 × 每秒移动组数”。原版装载端仍然
// 等待相邻建筑逐件 dump,所以单个物品源会先被 100/s 的输出上限卡住。
// 磁铜传送带在装载状态下从相邻 ItemSource 或 Drill 补满一组,使 25 件物品可以真正
// 每 3 tick 向前移动一次,即 25 × 60 / 3 = 500 件/秒。
function pullFrom(magneticConveyor, build, source, room){
    if(source == null || source == build.front() || source.team != build.team){
        return 0;
    }

    if(source.block instanceof ItemSource){
        // ItemSource 没有持久库存;outputItem 就是无限供应的配置物品。
        const item = source.outputItem;
        if(item == null || (build.items.any() && !build.items.has(item)) || !source.canDump(build, item)){
            return 0;
        }

        build.handleStack(item, room, source);
        return room;
    }

    if(source.block instanceof Drill){
        // 钻头只会产出 dominantItem,因此可以安全地批量取走该物品;不能
        // 使用 items.first(),否则将来扩展钻头时可能误取其他内部物品。
        const item = source.dominantItem;
        if(item == null || (build.items.any() && !build.items.has(item)) ||
            !source.items.has(item) || !source.canDump(build, item)){
            return 0;
        }

        const amount = Math.min(room, source.items.get(item));
        const removed = source.removeStack(item, amount);
        if(removed > 0){
            build.handleStack(item, removed, source);
        }
        return removed;
    }

    // 普通工厂、核心和仓库仍沿用原版输入规则,避免传送带错误抽走
    // 配方原料或绕过卸载器。
    return 0;
}

function fillLoadingDock(magneticConveyor, build){
    if(build.items.total() >= magneticConveyor.itemCapacity){
        return;
    }

    let room = magneticConveyor.itemCapacity - build.items.total();
    for(let i = 0; i < build.proximity.size && room > 0; i++){
        room -= pullFrom(magneticConveyor, build, build.proximity.get(i), room);
    }
}

function direction4(value){
    const result = value % 4;
    return result < 0 ? result + 4 : result;
}

function sideNeighbors(build){
    return [
        build.nearby(direction4(build.rotation + 1)),
        build.nearby(direction4(build.rotation + 3))
    ];
}

function nearbyInDirection(build, dir){
    const offset = Geometry.d4[dir];
    return Vars.world.build(
        build.tile.x + offset.x,
        build.tile.y + offset.y
    );
}

function isRenderConnection(magneticConveyor, build, near){
    if(near == null || near.team != build.team){
        return false;
    }

    // 只有其中一端确实朝向另一端时才绘制接口。这样 T 形分支会打开,
    // 但只是平行贴放、并未形成物品通路的两条传送带仍会各自保留封边。
    if(near.block == magneticConveyor){
        return near.front() == build || near.back() == build ||
            build.front() == near || build.back() == near;
    }

    // 正前方和正后方由本传送带自身的方向决定。
    if(build.front() == near || build.back() == near){
        return near.block.acceptsItems || near.block.outputsItems();
    }

    // 两侧只为真正朝向本传送带的输出端,或能够从侧面接货的建筑打开边框。
    // 有方向的物流方块必须以正确的一端相接,避免仅仅贴在旁边也出现缺口。
    if(near.block.rotate){
        return near.front() == build || near.back() == build;
    }

    return near.block.acceptsItems || near.block.outputsItems();
}

function canOutputToSide(magneticConveyor, build, target, item){
    if(target == null || target.team != build.team || item == null){
        return false;
    }

    // 磁铜传送带分支必须背对当前传送带,即真正从主线向侧面延伸。
    if(target.block == magneticConveyor){
        return target.back() == build && target.link == -1 && target.items.empty();
    }

    // 普通有方向的物流方块也必须背对主线,防止把物品倒灌进输入支线。
    if(target.block.rotate && target.back() != build){
        return false;
    }

    return target.acceptStack(item, build.items.get(item), build) > 0;
}

function chooseSideOutput(magneticConveyor, build){
    if(build.link == -1 || !build.enabled || build.state == 2 || build.items.empty()){
        return null;
    }

    const eff = build.enabled ? build.efficiency + magneticConveyor.baseEfficiency : 1;
    const nextCooldown = Math.max(
        0,
        build.cooldown - magneticConveyor.speed * eff * build.delta()
    );
    if(nextCooldown > 0){
        return null;
    }

    const item = build.lastItem != null && build.items.has(build.lastItem)
        ? build.lastItem
        : build.items.first();

    // 装载端仍然等到一整组再发送;中间节点可以转发侧面刚送来的整组。
    if(build.state == 1 && build.items.total() < magneticConveyor.itemCapacity){
        return null;
    }

    const sides = sideNeighbors(build).filter(target =>
        canOutputToSide(magneticConveyor, build, target, item)
    );
    if(sides.length == 0){
        return null;
    }

    // 正前方算一路,与有效的左右支路轮流分配整组物品。
    const route = build.cdump % (sides.length + 1);
    build.cdump++;
    return route == 0 ? null : sides[route - 1];
}

function moveToSide(magneticConveyor, build, target){
    const item = build.lastItem != null && build.items.has(build.lastItem)
        ? build.lastItem
        : build.items.first();
    if(item == null){
        return false;
    }

    if(target.block == magneticConveyor){
        if(target.link != -1 || !target.items.empty()){
            return false;
        }

        // 与原版 StackConveyor 的正向交接一致:整组和动画来源一起交给支路。
        target.items.add(build.items);
        target.lastItem = item;
        target.link = build.tile.pos();
        target.cooldown = 1;

        build.items.clear();
        build.lastItem = null;
        build.link = -1;
        build.cooldown = magneticConveyor.recharge;
        return true;
    }

    const accepted = Math.min(
        build.items.get(item),
        target.acceptStack(item, build.items.get(item), build)
    );
    if(accepted <= 0){
        return false;
    }

    build.items.remove(item, accepted);
    target.handleStack(item, accepted, build);
    build.cooldown = magneticConveyor.recharge;

    if(!build.items.has(item)){
        build.lastItem = null;
        build.link = -1;
    }
    return true;
}

Events.on(ContentInitEvent, cons(() => {
    const magneticConveyor = Vars.content.getByName(
        ContentType.block,
        modName + "-磁铜传送带"
    );

    if(magneticConveyor == null){
        Log.err("找不到磁铜传送带内容:" + modName + "-磁铜传送带");
        return;
    }

    const magneticSideRegion = Core.atlas.find(
        magneticConveyor.name + "-side"
    );
    const magneticBorderRegion = Core.atlas.find(
        magneticConveyor.name + "-border"
    );
    // border 贴图宽 6 像素,即 1.5 世界单位;将中心平移 3.25 单位后,
    // 它的外沿恰好落在 8×8 方块的边界上。
    const magneticBorderOffset = Vars.tilesize / 2 - 0.75;

    magneticConveyor.buildType = prov(() => extend(
        StackConveyor.StackConveyorBuild,
        magneticConveyor,
        {
            // 原版只允许 stateLoad 装载端收货。这里改为普通传送带式规则:
            // 后方和左右两侧均可输入,只有正前方禁止反向输入。
            acceptItem(source, item){
                if(this == source){
                    return this.items.total() < magneticConveyor.itemCapacity &&
                        (!this.items.any() || this.items.has(item));
                }
                if(source == this.front() || this.cooldown > magneticConveyor.recharge - 1){
                    return false;
                }
                return this.items.total() < magneticConveyor.itemCapacity &&
                    (!this.items.any() || this.items.has(item));
            },

            acceptStack(item, amount, source){
                if(source == this.front() || (this.items.any() && !this.items.has(item))){
                    return 0;
                }
                return Math.min(
                    amount,
                    magneticConveyor.itemCapacity - this.items.total()
                );
            },

            updateTile(){
                const side = chooseSideOutput(magneticConveyor, this);
                if(side != null){
                    // 暂时阻止父类在同一 tick 把这一组送往正前方;父类仍负责
                    // 更新冷却、供电状态和其他内部字段。
                    this.cooldown = magneticConveyor.recharge;
                    this.super$updateTile();
                    this.cooldown = 0;

                    if(moveToSide(magneticConveyor, this, side)){
                        fillLoadingDock(magneticConveyor, this);
                        return;
                    }
                }

                this.super$updateTile();
                fillLoadingDock(magneticConveyor, this);
            },

            drawCached(){
                // 必须和原版 StackConveyor 一样在缓存层中先画底图、再画 edgeRegion。
                // edgeRegion 的紫色内沿位于底图范围内;若放到 draw() 的动态层绘制,
                // 会被已缓存的底图遮住,只剩铜色角块,看起来就像没有封边。
                Draw.rect(
                    magneticConveyor.regions[this.state],
                    this.x,
                    this.y,
                    this.rotdeg()
                );

                // v159 的 Rhino 会错误复用 for 块内用 const 声明的值,导致四次循环
                // 都检查同一个方向。这里使用 var 显式重赋值,确保依次检查四条边。
                for(var i = 0; i < 4; i++){
                    var dir = direction4(this.rotation - i);
                    var near = nearbyInDirection(this, dir);
                    var connected = isRenderConnection(
                        magneticConveyor,
                        this,
                        near
                    );

                    if(!connected){
                        var offset = Geometry.d4[dir];
                        Draw.rect(
                            magneticBorderRegion,
                            this.x + offset.x * magneticBorderOffset,
                            this.y + offset.y * magneticBorderOffset,
                            dir * 90
                        );
                    }else if(i == 1 || i == 3){
                        // 侧向连接使用专用接口贴图填平接缝;
                        // 前后连接由相邻底图自然衔接。
                        Draw.rect(
                            magneticSideRegion,
                            this.x,
                            this.y,
                            dir * 90
                        );
                    }
                }
            },

            onProximityUpdate(){
                this.super$onProximityUpdate();
                if(Vars.headless){
                    return;
                }

                // 原版 blends() 不认识本模组允许的双向侧接规则,因此在父类完成
                // 状态计算后重建 blendprox,并再次缓存最终连接贴图。
                var mask = 0;
                for(var i = 0; i < 4; i++){
                    var dir = direction4(this.rotation - i);
                    if(isRenderConnection(
                        magneticConveyor,
                        this,
                        nearbyInDirection(this, dir)
                    )){
                        mask |= 1 << i;
                    }
                }

                this.blendprox = mask;
                this.recache();
            }
        }
    ));
}));