// SPDX-License-Identifier: AGPL-3.0-or-later
package preservation;

/**
 * Normal NO3 type0 EntityBreakable, selected EntityPrizeDrop and param0
 * EntityExplosion adapted from Xeeynamo/sotn-decomp commit
 * 0622fcfd37311fd21baf2938c6bd74cea63005ba: src/st/e_breakable.h,
 * no3/e_breakable.c, e_collect.h, st_common.h and create_entity.h.
 * Definitions, animation bytes and all placements are registered from disc.
 *
 * High-byte PreventRespawn bits persist through same-room camera movement;
 * original normal-room initialization clears them. The authored local reset/
 * checkpoint/room-entry lifecycle also clears them, retaining the explicit
 * Cube-of-Zoe test toggle. Equipped Alucard subweapon is checkpointed; room
 * actors, relic events and loose items are not serialized.
 * Camera entry scan replaces original directional layout cursors/all-type
 * slot competition. Sword/player pickup overlap and heart maximum299 belong to
 * GameEngine's authored test rules. Registered IDs0..11/14..22 implement
 * hearts/gold and subweapon pickup/exchange; other prize IDs remain unsupported, never
 * substituted. Selected original terrain queries are reused; unsupported
 * extended types stop conservatively and increment an exposed probe counter.
 * Subweapon-origin drops use original pickup locks in a bounded overlap proxy;
 * prior heart/gold overlap timing is retained. One logical sparkle per actor
 * replaces the shared PSX primitive allocator. Sound, subweapon attacks, other
 * prizes/relics/inventory and full GPU blending are unfinished.
 */
public final class CandleWorld {
    static final int ONE=1<<16,CANDLE=0,HEART=1,UNSUPPORTED=2,DESTROYED=3;
    private static final int ROOMS=28,TUPLE=6,MAX_LAYOUT=128,EFFECTS=32;
    private Definition candleDefinition,heartDefinition,explosionDefinition;
    private final Definition[] prizeDefinitions=new Definition[23];
    private int[] prizeCollisionSensors;
    private int[] prizeToInventory,inventoryToPrize;
    private final Layout[][] layouts=new Layout[ROOMS][];
    private final int[] prevented=new int[8];
    private final Effect[] effects=new Effect[EFFECTS];
    private StageMap.Room room;
    private Actor[] actors=new Actor[0];
    private boolean cubeOfZoeActive;
    private int effectSerial,effectCapacityMisses,collectedGold,equippedSubweapon,playerFeetY,playerFace=1,gameTimer,sparkleCapacity=MAX_LAYOUT,sparkleMisses;
    private static final class Definition {
        final int[] init,enemy,animation;
        final int velocityY,prizeValue,inventoryId;
        Definition(int[] init,int[] enemy,int[] animation,int velocityY){this(init,enemy,animation,velocityY,0);}
        Definition(int[] init,int[] enemy,int[] animation,int velocityY,int prizeValue){this(init,enemy,animation,velocityY,prizeValue,0);}
        Definition(int[] init,int[] enemy,int[] animation,int velocityY,int prizeValue,int inventoryId){this.init=init.clone();this.enemy=enemy.clone();this.animation=animation.clone();this.velocityY=velocityY;this.prizeValue=prizeValue;this.inventoryId=inventoryId;}
    }
    private static final class Layout {
        final int index,x,y,rawId,params,rawRoomIndex,slot,persistenceId;
        Layout(int[] values,int offset){index=values[offset];x=values[offset+1];y=values[offset+2];rawId=values[offset+3];params=values[offset+4];rawRoomIndex=values[offset+5];slot=64+(rawRoomIndex&255);persistenceId=rawRoomIndex>>>8;}
    }
    static final class Actor extends EnemyTerrain.Body {
        final Layout layout;
        int phase,step,pose,poseTimer,frame,candleFrame,itemId,fallSpeed,gravity,aliveTimer,hitboxWidth,hitboxHeight,lastHit=-1;
        int pickupLock,stepSub,sparkleTimer,sparkleHalfSize,sparkleGray,lastCollectedItemId=-1;
        boolean active,broken,inWindow,hitPending,collectPending,collected,expired,subweaponOrigin,swapDrop,sparkleVisible,sparkleAllocated,whiteFlash;
        Actor(Layout layout){this.layout=layout;x=layout.x*ONE;y=layout.y*ONE;itemId=layout.params&0xFFF;}
    }
    static final class Effect {
        final int slot,serial,sourceLayout;
        int x,y,step,pose,poseTimer,frame;
        boolean active=true;
        Effect(int slot,int serial,Actor source){this.slot=slot;this.serial=serial;sourceLayout=source.layout.index;x=source.x;y=source.y;}
    }
    private static boolean definitionValid(int[] init,int[] enemy,int expectedId){
        if(init==null||init.length!=5||enemy==null||enemy.length!=7)return false;
        for(int value:init)if(value<0||value>65535)return false;
        if(init[0]!=3||init[1]!=0||init[2]!=0||init[3]!=0||init[4]!=expectedId)return false;
        return enemy[0]>=1&&enemy[0]<=32767&&enemy[1]>=0&&enemy[1]<=32767&&enemy[2]>=0&&enemy[2]<=65535&&enemy[3]>=0&&enemy[3]<=65535&&enemy[4]>=0&&enemy[4]<=64&&enemy[5]>=0&&enemy[5]<=64;
    }
    private static boolean animationValid(int[] pairs,int minFrame,int maxFrame){
        if(pairs==null||pairs.length<4||pairs.length>512||pairs.length%2!=0)return false;
        for(int i=0;i<pairs.length-2;i+=2)if(pairs[i]<1||pairs[i]>254||pairs[i+1]<minFrame||pairs[i+1]>maxFrame)return false;
        return pairs[pairs.length-2]==0&&pairs[pairs.length-1]==0;
    }
    public boolean registerCandleDefinition(int[] init,int[] enemy,int[] animation){
        if(!definitionValid(init,enemy,0)||!animationValid(animation,1,2)||enemy[4]<1)return false;
        candleDefinition=new Definition(init,enemy,animation,0);reset(room);return true;
    }
    public boolean registerHeartDefinition(int[] init,int[] enemy,int[] animation){
        if(!definitionValid(init,enemy,1)||!animationValid(animation,14,14)||enemy[5]<1)return false;
        heartDefinition=new Definition(init,enemy,animation,0,1);reset(room);return true;
    }
    public boolean registerPrizeDefinition(int itemId,int[] init,int[] enemy,int[] animation,int value){
        if(itemId<1||itemId>11||!definitionValid(init,enemy,1)||enemy[4]<1||enemy[5]<1||!animationValid(animation,itemId==1?3:20,itemId==1?13:41)||value<1||value>(itemId==1?99:999999))return false;
        prizeDefinitions[itemId]=new Definition(init,enemy,animation,0,value);reset(room);return true;
    }
    public boolean registerPrizeCollisionSensors(int[] sensors){
        if(sensors==null||sensors.length!=4)return false;for(int value:sensors)if(value<-64||value>64)return false;
        prizeCollisionSensors=sensors.clone();reset(room);return true;
    }
    public boolean registerSubweaponDefinition(int itemId,int[] init,int[] enemy,int[] animation,int inventoryId){
        if(itemId<14||itemId>22||inventoryId<1||inventoryId>9||!definitionValid(init,enemy,1)||enemy[4]<1||enemy[5]<1||!animationValid(animation,42,113))return false;
        if(prizeToInventory!=null&&prizeToInventory[itemId-14]!=inventoryId)return false;
        prizeDefinitions[itemId]=new Definition(init,enemy,animation,0,0,inventoryId);reset(room);return true;
    }
    public boolean registerSubweaponMappings(int[] forward,int[] inverse){
        if(forward==null||forward.length!=9||inverse==null||inverse.length!=10||inverse[0]!=0)return false;
        for(int i=0;i<9;i++){
            int id=forward[i];if(id<1||id>9||inverse[id]!=14+i)return false;
            for(int j=0;j<i;j++)if(forward[j]==id)return false;
            if(prizeDefinitions[14+i]!=null&&prizeDefinitions[14+i].inventoryId!=id)return false;
        }
        for(int i=1;i<10;i++)if(inverse[i]<14||inverse[i]>22)return false;
        prizeToInventory=forward.clone();inventoryToPrize=inverse.clone();reset(room);return true;
    }
    private boolean supportedPrize(int itemId){return prizeDefinition(itemId)!=null&&(itemId==0||prizeCollisionSensors!=null)&&(itemId<14||prizeToInventory!=null&&prizeDefinitions[1]!=null);}
    public int equippedSubweapon(){return equippedSubweapon;}
    public int equippedPrizeId(){return inventoryToPrize==null?0:inventoryToPrize[equippedSubweapon];}
    public void setEquippedSubweapon(int id){if(id<0||id>9)throw new IllegalArgumentException("Invalid equipped subweapon");equippedSubweapon=id;}
    private Definition prizeDefinition(int itemId){return itemId==0?heartDefinition:itemId>0&&itemId<prizeDefinitions.length?prizeDefinitions[itemId]:null;}
    public boolean registerExplosionDefinition(int[] init,int[] enemy,int[] animation,int velocityY){
        if(!definitionValid(init,enemy,2)||!animationValid(animation,1,8)||velocityY!=-ONE)return false;
        explosionDefinition=new Definition(init,enemy,animation,velocityY);reset(room);return true;
    }
    public boolean registerLayouts(StageMap.Room target,int[] tuples){
        if(target==null||target.id<0||target.id>=ROOMS||target.redirectRoom>=0||tuples==null||tuples.length%TUPLE!=0||tuples.length>MAX_LAYOUT*TUPLE)return false;
        Layout[] proposed=new Layout[tuples.length/TUPLE];
        for(int i=0;i<proposed.length;i++){
            Layout entry=new Layout(tuples,i*TUPLE);
            if(entry.index<0||entry.index>65535||entry.x<0||entry.x>=target.width||entry.y<0||entry.y>=target.height||entry.rawId!=0xA001||entry.params<0||entry.params>=0x1000||entry.rawRoomIndex<0||entry.rawRoomIndex>65535||(entry.rawRoomIndex&255)>=192)return false;
            for(int j=0;j<i;j++)if(proposed[j].index==entry.index||proposed[j].slot==entry.slot)return false;
            proposed[i]=entry;
        }
        layouts[target.id]=proposed;if(room==target)reset(target);return true;
    }
    public void setCubeOfZoeActive(boolean enabled){cubeOfZoeActive=enabled;}
    public boolean cubeOfZoeActive(){return cubeOfZoeActive;}
    public void clear(){for(int i=0;i<layouts.length;i++)layouts[i]=null;reset(null);}
    public void reset(StageMap.Room target){
        room=target;for(int i=0;i<prevented.length;i++)prevented[i]=0;for(int i=0;i<effects.length;i++)effects[i]=null;effectSerial=effectCapacityMisses=collectedGold=gameTimer=sparkleMisses=0;
        Layout[] list=room==null?null:layouts[room.id];actors=list==null?new Actor[0]:new Actor[list.length];for(int i=0;i<actors.length;i++)actors[i]=new Actor(list[i]);
    }
    private boolean prevented(Layout layout){if(layout.persistenceId==0)return false;int bit=layout.persistenceId-1;return(prevented[bit>>>5]&(1<<(bit&31)))!=0;}
    private void prevent(Layout layout){if(layout.persistenceId!=0){int bit=layout.persistenceId-1;prevented[bit>>>5]|=1<<(bit&31);}}
    private static int animate(Actor actor,int[] pairs){
        if(actor.poseTimer==0){int duration=pairs[actor.pose*2];if(duration==0){actor.pose=1;actor.poseTimer=pairs[0];actor.frame=pairs[1];return 0;}actor.poseTimer=duration;actor.frame=pairs[actor.pose*2+1];actor.pose++;}
        actor.poseTimer--;return 1;
    }
    private static int animate(Effect effect,int[] pairs){
        if(effect.poseTimer==0){int duration=pairs[effect.pose*2];if(duration==0){effect.pose=1;effect.poseTimer=pairs[0];effect.frame=pairs[1];return 0;}effect.poseTimer=duration;effect.frame=pairs[effect.pose*2+1];effect.pose++;}
        effect.poseTimer--;return 1;
    }
    private void activate(Actor actor){
        actor.x=actor.layout.x*ONE;actor.y=actor.layout.y*ONE;actor.vx=actor.vy=0;actor.phase=CANDLE;actor.step=actor.pose=actor.poseTimer=actor.frame=0;actor.active=true;actor.hitPending=actor.collectPending=actor.broken=actor.collected=actor.expired=false;actor.lastHit=-1;actor.unsupportedQueries=0;
        actor.fallSpeed=actor.gravity=actor.aliveTimer=actor.candleFrame=actor.flags=actor.hitboxWidth=actor.hitboxHeight=0;actor.itemId=actor.layout.params&0xFFF;
        actor.pickupLock=actor.stepSub=actor.sparkleTimer=actor.sparkleHalfSize=actor.sparkleGray=0;actor.lastCollectedItemId=-1;actor.subweaponOrigin=actor.swapDrop=actor.sparkleVisible=actor.sparkleAllocated=actor.whiteFlash=false;
    }
    /** Returns original heart prize total; goldCollected() is the current tick's gold. */
    public int update(int cameraX,int cameraY){
        return update(cameraX,cameraY,playerFeetY,playerFace,gameTimer+1);
    }
    public int update(int cameraX,int cameraY,int feetY,int face,int timer){
        return update(cameraX,cameraY,feetY,face,timer,false);
    }
    public int update(int cameraX,int cameraY,int feetY,int face,int timer,boolean timeStopped){
        playerFeetY=feetY;playerFace=face;gameTimer=timer;
        collectedGold=0;if(room==null||candleDefinition==null)return 0;int collected=0;
        for(Actor actor:actors){
            boolean inWindow=actor.layout.x>=Math.max(0,cameraX-64)&&actor.layout.x<=cameraX+320&&actor.layout.y>=Math.max(0,cameraY-64)&&actor.layout.y<=cameraY+288;
            if(inWindow&&!actor.inWindow&&!prevented(actor.layout))activate(actor);actor.inWindow=inWindow;
            if(!actor.active)continue;
            if(actor.phase==CANDLE){
                int sx=(actor.x>>16)-cameraX,sy=(actor.y>>16)-cameraY;
                if(actor.step>0&&(candleDefinition.enemy[6]&0x80000000)!=0&&(sx<-128||sx>384||sy<-128||sy>352)){actor.active=false;continue;}
                if(actor.step>0&&(candleDefinition.enemy[6]&0x20000000)!=0&&(sx<-64||sx>320||sy<-64||sy>288))continue;
                if(!StageUpdateGate.shouldRun(actor.step,actor.flags,timeStopped,timer))continue;
                if(actor.step==0){actor.flags=candleDefinition.enemy[6];actor.hitboxWidth=candleDefinition.enemy[4];actor.hitboxHeight=8;actor.frame=candleDefinition.init[1];actor.step=1;}
                else{animate(actor,candleDefinition.animation);actor.candleFrame=actor.frame;if(actor.hitPending)breakCandle(actor);}
                actor.hitPending=false;
            }else if(actor.phase==HEART&&StageUpdateGate.shouldRun(actor.step,actor.flags,timeStopped,timer))collected+=updateHeart(actor);
        }
        // Original224..255 update after candle slots, initializing new effects
        // on the same break update; a same-slot prize waits the next update.
        if(explosionDefinition!=null)for(Effect effect:effects)if(effect!=null&&effect.active){
            int sx=(effect.x>>16)-cameraX,sy=(effect.y>>16)-cameraY;boolean barely=(explosionDefinition.enemy[6]&0x40000000)!=0;
            if(effect.step>0&&(explosionDefinition.enemy[6]&0x80000000)!=0&&(sx<(barely?-64:-128)||sx>(barely?320:384)||sy<(barely?-64:-128)||sy>(barely?288:352))){effect.active=false;continue;}
            if(!StageUpdateGate.shouldRun(effect.step,explosionDefinition.enemy[6],timeStopped,timer))continue;
            if(effect.step==0){effect.frame=explosionDefinition.init[1];effect.step=1;}
            else{effect.y+=explosionDefinition.velocityY;if(animate(effect,explosionDefinition.animation)==0)effect.active=false;}
        }
        return collected;
    }
    private void breakCandle(Actor actor){
        spawnExplosion(actor);prevent(actor.layout);actor.broken=true;
        if(!cubeOfZoeActive){actor.phase=DESTROYED;actor.active=false;return;}
        actor.itemId=actor.layout.params&0xFFF;actor.pose=actor.poseTimer=actor.step=0;
        if(supportedPrize(actor.itemId)){actor.phase=HEART;actor.collectPending=false;actor.subweaponOrigin=actor.itemId>=14&&actor.itemId<=22;if(actor.subweaponOrigin)actor.pickupLock=16;}
        else{actor.phase=UNSUPPORTED;actor.active=false;actor.frame=0;}
    }
    private void spawnExplosion(Actor actor){
        if(explosionDefinition==null)return;for(int i=0;i<effects.length;i++)if(effects[i]==null||!effects[i].active){effects[i]=new Effect(224+i,++effectSerial,actor);return;}effectCapacityMisses++;
    }
    private EnemyTerrain.Collision probe(Actor actor,int x,int y){EnemyTerrain.Collision col=EnemyTerrain.query(room,x,y);if(col.unsupported)actor.unsupportedQueries++;return col;}
    private static void prizeFall(Actor actor){
        if(actor.vy>=0){actor.fallSpeed+=actor.gravity;actor.vx=actor.fallSpeed;if(actor.vx==ONE||actor.vx==-ONE)actor.gravity=-actor.gravity;}
        if(actor.vy<ONE/4)actor.vy+=ONE/8;
    }
    private static void normalFall(Actor actor){if(actor.vy<6*ONE)actor.vy+=ONE/4;}
    private void groundedFall(Actor actor){
        // Preserve the original unusual X-velocity test for the top probe.
        if(actor.vx<0){EnemyTerrain.Collision top=probe(actor,actor.x>>16,(actor.y>>16)-7);if((top.effects&5)!=0)actor.vy=0;if(top.unsupported)return;}
        EnemyTerrain.Collision bottom=probe(actor,actor.x>>16,(actor.y>>16)+7);if(bottom.unsupported)return;
        if(actor.itemId!=0){
            if((bottom.effects&5)==0){actor.x+=actor.vx;actor.y+=actor.vy;normalFall(actor);}
            else{actor.vx=actor.vy=0;if((bottom.effects&4)!=0)actor.y+=ONE/8;else actor.y+=bottom.up*ONE;}
        }else if((bottom.effects&5)==0){actor.x+=actor.vx;actor.y+=actor.vy;prizeFall(actor);}
    }
    private int updateHeart(Actor actor){
        Definition definition=prizeDefinition(actor.itemId);
        if(actor.step!=0)animate(actor,definition.animation);
        if(actor.step>1&&actor.step<5&&actor.collectPending)actor.step=5;
        actor.collectPending=false;
        actor.whiteFlash=actor.pickupLock>=24&&(gameTimer&2)==0&&actor.itemId!=1;
        switch(actor.step){
        case 0:actor.flags=definition.enemy[6];actor.frame=definition.init[1];if(actor.itemId>=14&&actor.itemId<=22&&actor.itemId==equippedPrizeId())actor.itemId=1;actor.hitboxWidth=actor.itemId==0||actor.itemId==2?4:definition.enemy[4];actor.hitboxHeight=definition.enemy[5];actor.step=1;break;
        case 1:{EnemyTerrain.Collision center=probe(actor,actor.x>>16,actor.y>>16);if((center.effects&7)!=0||center.unsupported){actor.active=false;actor.expired=true;}else actor.step=2;if(actor.itemId==0){actor.fallSpeed=-ONE;actor.gravity=0x800;}break;}
        case 2:{
            if(actor.vy<0){EnemyTerrain.Collision top=probe(actor,actor.x>>16,(actor.y>>16)-7);if(top.unsupported)break;if((top.effects&5)!=0)actor.vy=0;}
            int oldX=actor.x,oldY=actor.y;actor.x+=actor.vx;actor.y+=actor.vy;
            EnemyTerrain.Collision bottom=probe(actor,actor.x>>16,(actor.y>>16)+7);
            if(bottom.unsupported){actor.x=oldX;actor.y=oldY;break;}
            if(actor.itemId!=0){
                if((bottom.effects&5)!=0&&actor.vy>0){actor.vx=actor.vy=0;actor.y+=bottom.up*ONE;actor.aliveTimer=240;actor.step=3;}else normalFall(actor);
                EnemyTerrain.checkField(room,actor,prizeCollisionSensors);
            }else if((bottom.effects&5)!=0){actor.y+=bottom.up*ONE;actor.aliveTimer=96;actor.step=3;}else prizeFall(actor);break;
        }
        case 3:groundedFall(actor);if(--actor.aliveTimer==0){actor.aliveTimer=actor.itemId==0?64:80;actor.step=4;}break;
        case 4:groundedFall(actor);if(--actor.aliveTimer!=0){if((actor.aliveTimer&2)!=0)actor.frame=0;}else{actor.active=false;actor.expired=true;}break;
        case 5:actor.lastCollectedItemId=actor.itemId;actor.collected=true;if(actor.itemId>=14&&actor.itemId<=22){collectSubweapon(actor);return 0;}actor.active=false;actor.sparkleAllocated=false;if(actor.itemId<2)return definition.prizeValue;collectedGold+=definition.prizeValue;return 0;
        case 7:updateExchange(actor);break;
        default:throw new IllegalStateException("Invalid prize step");
        }
        return 0;
    }
    private void collectSubweapon(Actor actor){
        int previous=equippedSubweapon;equippedSubweapon=prizeToInventory[actor.itemId-14];
        int outgoing=previous==equippedSubweapon?1:inventoryToPrize[previous];
        actor.pickupLock=previous==equippedSubweapon?16:96;
        if(outgoing==0){actor.active=false;actor.sparkleAllocated=false;return;}
        // Registration can be partial in an editable reconstruction. Preserve
        // the source inventory assignment and expose missing data explicitly.
        if(!supportedPrize(outgoing)){actor.itemId=outgoing;actor.phase=UNSUPPORTED;actor.active=false;actor.frame=actor.step=0;actor.sparkleAllocated=false;return;}
        // Original integer-half assignment retains the old item's fraction.
        actor.itemId=outgoing;actor.y=((playerFeetY-12)<<16)|(actor.y&0xFFFF);
        actor.step=7;actor.stepSub=actor.pose=actor.poseTimer=actor.frame=0;
        actor.vy=-5*ONE/2;actor.vx=playerFace==1?-2*ONE:2*ONE;
        actor.sparkleTimer=5;actor.sparkleHalfSize=0;actor.sparkleGray=128;actor.sparkleVisible=false;actor.swapDrop=true;actor.collected=false;
    }
    private void updateExchange(Actor actor){
        switch(actor.stepSub){
        case 0:
            actor.frame=0;if(actor.itemId>=14&&actor.itemId<=22&&actor.itemId==equippedPrizeId())actor.itemId=1;
            // One bounded logical primitive; shared original GPU contention is
            // not reconstructed. Its source timing and geometry are retained.
            int allocated=0;for(Actor other:actors)if(other.active&&other.sparkleAllocated)allocated++;
            if(allocated<sparkleCapacity){actor.sparkleAllocated=true;actor.sparkleGray=128;actor.sparkleVisible=false;actor.stepSub=1;}else sparkleMisses++;break;
        case 1:{
            int oldX=actor.x,oldY=actor.y;actor.x+=actor.vx;actor.y+=actor.vy;
            EnemyTerrain.Collision bottom=probe(actor,actor.x>>16,(actor.y>>16)+7);
            if(bottom.unsupported){actor.x=oldX;actor.y=oldY;actor.frame=0;break;}
            if((bottom.effects&5)!=0&&actor.vy>0){actor.vx=actor.vy=0;actor.y+=bottom.up*ONE;actor.stepSub=2;}else normalFall(actor);
            EnemyTerrain.checkField(room,actor,prizeCollisionSensors);actor.frame=0;
            if(actor.sparkleTimer!=0)actor.sparkleTimer--;else{actor.sparkleHalfSize=1;actor.sparkleVisible=true;}break;
        }
        case 2:
            groundedFall(actor);actor.sparkleTimer++;
            if(actor.sparkleTimer<17){actor.sparkleHalfSize=actor.sparkleTimer;actor.frame=0;}
            else{actor.sparkleHalfSize=32-actor.sparkleTimer;actor.sparkleGray-=8;}
            if(actor.sparkleTimer==32){actor.sparkleAllocated=actor.sparkleVisible=false;actor.aliveTimer=208;actor.step=3;actor.stepSub=0;}break;
        default:throw new IllegalStateException("Invalid exchange substep");
        }
    }
    public int hitSword(int swordX,int swordY,int halfWidth,int halfHeight,int sequence){
        int hits=0;for(Actor actor:actors)if(actor.phase==CANDLE&&actor.active&&actor.step>0&&!actor.hitPending&&actor.lastHit!=sequence&&Math.abs((actor.x>>16)-swordX)<actor.hitboxWidth+halfWidth&&Math.abs((actor.y>>16)-swordY)<actor.hitboxHeight+halfHeight){actor.hitPending=true;actor.lastHit=sequence;hits++;}return hits;
    }
    public void markPickups(int playerX,int feetY,int height){
        for(Actor actor:actors)if(!actor.subweaponOrigin&&pickupOverlap(actor,playerX,feetY,height))actor.collectPending=true;
    }
    private static boolean pickupOverlap(Actor actor,int playerX,int feetY,int height){return actor.phase==HEART&&actor.active&&actor.step>1&&actor.step<5&&Math.abs((actor.x>>16)-playerX)<actor.hitboxWidth+7&&(actor.y>>16)+actor.hitboxHeight>feetY-height+1&&(actor.y>>16)-actor.hitboxHeight<feetY-1;}
    /** Bounded pre-update HitDetection proxy for new subweapon-origin drops. */
    public void markSubweaponPickups(int playerX,int feetY,int height,int cameraX,int cameraY){
        for(Actor actor:actors)if(actor.subweaponOrigin&&actor.phase==HEART&&actor.active&&actor.hitboxWidth!=0&&actor.hitboxHeight!=0){
            if(actor.pickupLock!=0)actor.pickupLock--;
            int sx=(actor.x>>16)-cameraX,sy=(actor.y>>16)-cameraY;
            if(sx>-32&&sx<288&&sy>-32&&sy<256&&actor.pickupLock==0&&pickupOverlap(actor,playerX,feetY,height))actor.collectPending=true;
        }
    }
    private static void position(StringBuilder out,int x,int y){out.append(",\"x\":").append(x>>16).append(",\"y\":").append(y>>16).append(",\"xFixed\":").append(x).append(",\"yFixed\":").append(y);}
    public String getCandles(){
        StringBuilder out=new StringBuilder("[");for(int i=0;i<actors.length;i++){if(i!=0)out.append(',');Actor a=actors[i];out.append("{\"kind\":\"candle\",\"roomId\":").append(room.id).append(",\"layoutIndex\":").append(a.layout.index).append(",\"rawEntityId\":").append(a.layout.rawId).append(",\"rawRoomIndex\":").append(a.layout.rawRoomIndex).append(",\"slot\":").append(a.layout.slot).append(",\"persistenceId\":").append(a.layout.persistenceId).append(",\"params\":").append(a.layout.params).append(",\"dropItemId\":").append(a.itemId);position(out,a.layout.x*ONE,a.layout.y*ONE);out.append(",\"frame\":").append(a.phase==CANDLE?a.frame:a.candleFrame).append(",\"step\":").append(a.phase==CANDLE?a.step:0).append(",\"facingLeft\":0,\"face\":1,\"palette\":0,\"blendMode\":112,\"broken\":").append(a.broken).append(",\"alive\":").append(!a.broken).append(",\"active\":").append(a.active&&a.phase==CANDLE).append('}');}return out.append(']').toString();
    }
    public String getDrops(){
        StringBuilder out=new StringBuilder("[");boolean comma=false;
        for(Actor a:actors)if(a.phase==HEART||a.phase==UNSUPPORTED){
            if(comma)out.append(',');comma=true;Definition definition=prizeDefinition(a.itemId);
            out.append("{\"kind\":\"").append(a.phase==UNSUPPORTED?"unsupportedDrop":a.itemId==0?"smallHeart":a.itemId==1?"largeHeart":a.itemId>=14&&a.itemId<=22?"subweapon":"gold");
            out.append("\",\"roomId\":").append(room.id).append(",\"sourceLayoutIndex\":").append(a.layout.index).append(",\"slot\":").append(a.layout.slot).append(",\"entityId\":").append(a.itemId<128?3:10).append(",\"itemId\":").append(a.itemId).append(",\"inventoryId\":").append(definition==null?0:definition.inventoryId).append(",\"supported\":").append(a.phase==HEART).append(",\"prizeInitialized\":").append(a.phase==HEART&&a.step>0).append(",\"prizeValue\":").append(definition==null?0:definition.prizeValue).append(",\"blendMode\":").append(a.step==0?112:0);
            position(out,a.x,a.y);
            out.append(",\"vxFixed\":").append(a.vx).append(",\"vyFixed\":").append(a.vy).append(",\"fallSpeedFixed\":").append(a.fallSpeed).append(",\"gravityFixed\":").append(a.gravity).append(",\"frame\":").append(a.frame).append(",\"step\":").append(a.step).append(",\"pose\":").append(a.pose).append(",\"poseTimer\":").append(a.poseTimer).append(",\"aliveTimer\":").append(a.aliveTimer);
            out.append(",\"stepSub\":").append(a.stepSub).append(",\"sparkleTimer\":").append(a.sparkleTimer).append(",\"pickupLock\":").append(a.pickupLock).append(",\"swapDrop\":").append(a.swapDrop).append(",\"lastCollectedItemId\":").append(a.lastCollectedItemId);
            out.append(",\"whiteFlash\":").append(a.whiteFlash).append(",\"swapSparkle\":{\"x\":").append(a.x>>16).append(",\"y\":").append(a.y>>16).append(",\"halfSize\":").append(a.sparkleHalfSize).append(",\"gray\":").append(a.sparkleGray).append(",\"visible\":").append(a.sparkleVisible&&a.active).append('}');
            out.append(",\"facingLeft\":0,\"face\":1,\"palette\":").append(a.whiteFlash?0x815F:0).append(",\"collected\":").append(a.collected).append(",\"expired\":").append(a.expired).append(",\"active\":").append(a.active).append(",\"alive\":").append(a.active).append(",\"unsupportedTerrainProbes\":").append(a.unsupportedQueries);
            if(a.phase==UNSUPPORTED)out.append(",\"reason\":\"This original prize branch/profile is not registered or implemented.\"");out.append('}');
        }
        return out.append(']').toString();
    }
    public String getEffects(){
        StringBuilder out=new StringBuilder("[");boolean comma=false;for(Effect e:effects)if(e!=null){if(comma)out.append(',');comma=true;out.append("{\"kind\":\"candleExplosion\",\"roomId\":").append(room.id).append(",\"slot\":").append(e.slot).append(",\"serial\":").append(e.serial).append(",\"sourceLayoutIndex\":").append(e.sourceLayout).append(",\"params\":0");position(out,e.x,e.y);out.append(",\"vyFixed\":").append(explosionDefinition.velocityY).append(",\"frame\":").append(e.frame).append(",\"step\":").append(e.step).append(",\"pose\":").append(e.pose).append(",\"poseTimer\":").append(e.poseTimer).append(",\"facingLeft\":0,\"face\":1,\"palette\":0,\"animSet\":2,\"blendMode\":48,\"active\":").append(e.active).append(",\"alive\":").append(e.active).append('}');}return out.append(']').toString();
    }
    // Native test/audit access; not exported into a player-facing debug API.
    Actor actor(int index){return actors[index];}int count(){return actors.length;}int preventWord(int index){return prevented[index];}Effect effect(int index){return effects[index];}int effectMisses(){return effectCapacityMisses;}
    public int goldCollected(){return collectedGold;}
    // Bounded logical allocator fault injection for native source-parity audits.
    void setSparkleCapacity(int capacity){if(capacity<0||capacity>MAX_LAYOUT)throw new IllegalArgumentException("Invalid sparkle capacity");sparkleCapacity=capacity;}
    int sparkleMisses(){return sparkleMisses;}
}
