package preservation;

import org.teavm.jso.JSExport;

/**
 * Editable, native Java game logic compiled to HTML5 by TeaVM.
 * No PSX CPU, BIOS, GPU, ROM execution, or emulator is involved.
 *
 * This is a reconstruction laboratory, NOT the complete original game.
 * Verified reference values: walk 1.5, jump -4.875, air acceleration 22/128,
 * terminal fall 7, release jump clip -1 pixels per frame. See provenance.json.
 * Animation images/timing are decoded from the user's ARC_F.BIN / DRA.BIN.
 * Room bounds and collision codes are decoded from the original stage data.
 * Body sensors, geometric room transitions, targets, checkpoints and combat
 * remain authored approximations. Vertical selection uses this engine's feet
 * coordinate, not the original entity-origin/camera/form transition timing.
 */
public final class GameEngine {
    public static final int ONE = 1 << 16;
    public static final int WALK = 98304;       // 1.5 * 65536
    public static final int JUMP = -319488;     // -4.875 * 65536
    public static final int GRAVITY = 11264;    // (22 / 128) * 65536
    public static final int TERMINAL = 458752;  // 7 * 65536
    public static final int GROUND_DRAG = 8192; // .125 * 65536
    public static final int AIR_DRAG = 4096;    // .0625 * 65536
    private static final int VIEW_WIDTH=320, VIEW_HEIGHT=240;
    private static final int HALF_WIDTH=7, HEIGHT=40;
    private static final boolean[] keys=new boolean[256];
    private static Target[] targets=new Target[4];
    private static final StageMap stage=new StageMap();
    private static final EnemyWorld enemies=new EnemyWorld();
    private static final ZombieWorld zombies=new ZombieWorld();
    private static final CandleWorld candles=new CandleWorld();
    private static final DaggerWorld.SharedPool subweaponPool=new DaggerWorld.SharedPool();
    private static final DaggerWorld daggers=new DaggerWorld(subweaponPool);
    private static final AxeWorld axes=new AxeWorld(subweaponPool);
    private static final HolyWaterWorld holyWater=new HolyWaterWorld(subweaponPool);
    private static final CrossWorld crosses=new CrossWorld(subweaponPool,new CrossWorld.RandomSource(){public int next(){return holyWater.nextSelectedRandom();}});
    private static final BibleWorld bibles=new BibleWorld(subweaponPool,new BibleWorld.RandomSource(){public int next(){return holyWater.nextSelectedRandom();}});
    private static final StopwatchWorld stopwatches=new StopwatchWorld(subweaponPool,new StopwatchWorld.RandomSource(){public int next(){return holyWater.nextSelectedRandom();}});
    private static final ReboundWorld rebounds=new ReboundWorld(subweaponPool,new ReboundWorld.RandomSource(){public int next(){return holyWater.nextSelectedRandom();}});
    private static final VibhutiWorld vibhuti=new VibhutiWorld(subweaponPool,new VibhutiWorld.RandomSource(){public int next(){return holyWater.nextSelectedRandom();}});
    private static final AguneaWorld agunea=new AguneaWorld(subweaponPool,new AguneaWorld.RandomSource(){public int next(){return holyWater.nextSelectedRandom();}});
    private static final PlayerOutlineWorld playerOutlines=new PlayerOutlineWorld(subweaponPool);
    private static final PlayerBlinkWorld playerBlinks=new PlayerBlinkWorld(subweaponPool);
    private static AguneaTestTarget aguneaTestTarget;
    private static final class AguneaTestTarget {
        final int x,y,initialHp;int hp;
        AguneaTestTarget(int x,int y,int hp){this.x=x;this.y=y;this.hp=initialHp=hp;}
    }
    private static int batHitboxWidth,batHitboxHeight;
    private static final AxeWorld.Environment axeEnvironment=new AxeWorld.Environment(){
        public int hit(AxeWorld.Axe axe,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){return hitAxeObjects(axe,px,py,halfWidth,halfHeight,sequence,damage);}
    };
    private static final HolyWaterWorld.Environment holyWaterEnvironment=new HolyWaterWorld.Environment(){
        public EnemyTerrain.Collision query(int px,int py){
            if(sceneMode==2)return EnemyTerrain.query(stageRoom,px,py);
            EnemyTerrain.Collision result=new EnemyTerrain.Collision();result.up=-(py&15);result.left=-(px&15);result.right=result.left+15;result.effects=pixelSolid(px,py)?3:0;return result;
        }
        public int hit(HolyWaterWorld.Actor actor,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){return hitHolyWaterObjects(actor,px,py,halfWidth,halfHeight,sequence,damage);}
    };
    private static final DaggerWorld.Environment daggerEnvironment=new DaggerWorld.Environment(){
        public int effectsAt(int px,int py){
            if(sceneMode==2){EnemyTerrain.Collision collision=EnemyTerrain.query(stageRoom,px,py);return collision.effects|(collision.unsupported?DaggerWorld.UNKNOWN:0);}
            return pixelSolid(px,py)?3:0;
        }
        public int hit(int px,int py,int halfWidth,int halfHeight,int sequence,int damage){return hitDaggerObjects(px,py,halfWidth,halfHeight,sequence,damage);}
    };
    private static final CrossWorld.Environment crossEnvironment=new CrossWorld.Environment(){public int hit(CrossWorld.Actor actor,int px,int py,int width,int height,int sequence,int damage){return hitCrossObjects(actor,px,py,width,height,sequence,damage);}};
    private static final BibleWorld.Environment bibleEnvironment=new BibleWorld.Environment(){public int hit(BibleWorld.Actor actor,int px,int py,int width,int height,int sequence,int damage){return hitBibleObjects(actor,px,py,width,height,sequence,damage);}};
    private static final ReboundWorld.Environment reboundEnvironment=new ReboundWorld.Environment(){public ReboundTerrain.Collision query(int px,int py){if(sceneMode==2)return ReboundTerrain.query(stageRoom,px,py);ReboundTerrain.Collision c=new ReboundTerrain.Collision();c.effects=pixelSolid(px,py)?3:0;return c;}public int hit(ReboundWorld.Actor actor,int px,int py,int width,int height,int sequence,int damage){return hitReboundObjects(actor,px,py,width,height,sequence,damage);}};
    private static final VibhutiWorld.Environment vibhutiEnvironment=new VibhutiWorld.Environment(){public VibhutiTerrain.Collision query(int px,int py){if(sceneMode==2)return VibhutiTerrain.query(stageRoom,px,py);VibhutiTerrain.Collision c=new VibhutiTerrain.Collision();c.effects=pixelSolid(px,py)?3:0;c.up=-(py&15);return c;}public int hit(VibhutiWorld.Actor actor,int px,int py,int width,int height,int sequence,int damage){return hitVibhutiObjects(actor,px,py,width,height,sequence,damage);}};
    private static final AguneaWorld.Environment aguneaEnvironment=new AguneaWorld.Environment(){
        public AguneaWorld.Target capture(int px,int py,int width,int height,int damage,int sequence){return captureAguneaTarget(px,py,width,height,damage,sequence);}
        public AguneaWorld.Target resolve(Object identity){return resolveAguneaTarget(identity);}
        public void pulse(Object identity,int damage,int sequence){damageAguneaTarget(identity,damage,sequence);}
        public int hearts(){return hearts;}
        public boolean spend(int cost){if(cost<1||hearts<cost)return false;hearts-=cost;return true;}
        public boolean cancelled(){return false;} // Full transform/status system is unported.
    };
    // Authored local test inventory maximum; original inventory is not ported.
    private static final int HEART_TEST_MAX=299;
    private static final int GOLD_MAX=999999; // Original include/game.h MAX_GOLD.
    private static StageMap.Room stageRoom;
    private static int sceneMode,geometryFingerprint,unsupportedCollisionCount,checkpointRoomId=-1;
    private static String navigationNotice="";
    private static int tileSize=16, worldWidth=2048, worldHeight=256, columns=128, rows=16;
    private static int[] solids;
    private static boolean originalTileCodes;
    private static int x,y,vx,vy,face,animation,animationTick,tick,hp,hearts,hits,gold;
    private static int attackTimer,attackSequence,invulnerable,checkpointX,checkpointY;
    private static int throwPoseTimer,throwAnimation,useSubweaponTimer;
    private static String subweaponAttackOutcome="none",subweaponAttackNotice="";
    private static boolean grounded,jumpWasDown,attackWasDown,crouch,dead,checkpointCrouch;
    private static double accumulator;
    private static int spawnX=64, spawnY=208,spawnRoomId=-1;
    private static final class Target {
        int x,y,life,respawn,lastHit; Target(int a,int b){x=a;y=b;life=3;lastHit=-1;}
    }
    private GameEngine() {}

    @JSExport public static void init() {
        if(solids==null)useLaboratory();else reset();
    }
    private static void createLaboratory() {
        solids=new int[columns*rows];
        for(int c=0;c<columns;c++)solids[(rows-2)*columns+c]=1;
        platform(16,11,8);platform(34,9,7);platform(51,11,8);
        platform(72,8,6);platform(89,10,8);platform(106,11,8);
    }
    private static void platform(int col,int row,int count){for(int i=0;i<count;i++)solids[row*columns+col+i]=1;}
    @JSExport public static void useLaboratory(){sceneMode=0;stageRoom=null;originalTileCodes=false;tileSize=16;worldWidth=2048;worldHeight=256;columns=128;rows=16;spawnX=64;spawnY=208;createLaboratory();geometryFingerprint=hashGrid(solids);unsupportedCollisionCount=0;reset();}

    /** Load an explicit room grid; a 1 means a solid 16px test collider. */
    @JSExport public static boolean configure(int width,int height,int size,int[] grid,int startX,int startY) {
        if(width<256||width>16384||height<240||height>4096||size<4||size>64)return false;
        int cols=(width+size-1)/size, rs=(height+size-1)/size;
        if(grid==null||grid.length!=cols*rs||startX<HALF_WIDTH||startX>=width-HALF_WIDTH||startY<HEIGHT||startY>=height)return false;
        worldWidth=width;worldHeight=height;tileSize=size;columns=cols;rows=rs;
        sceneMode=1;stageRoom=null;originalTileCodes=false;solids=grid.clone();spawnX=startX;spawnY=startY;geometryFingerprint=hashGrid(solids);unsupportedCollisionCount=0;
        reset();return true;
    }
    @JSExport public static boolean configureOriginalRoom(int width,int height,int[] codes,int startX,int startY){
        if(!configure(width,height,16,codes,startX,startY))return false;
        originalTileCodes=true;unsupportedCollisionCount=countUnsupported(codes);reset();return true;
    }
    private static int hashGrid(int[] grid){int hash=17;for(int code:grid)hash=31*hash+code;return hash;}
    private static int countUnsupported(int[] grid){int count=0;for(int code:grid)if(!TileCollider.isSupported(code))count++;return count;}
    @JSExport public static boolean registerStageRoom(int id,int leftScreen,int topScreen,int rightScreen,int bottomScreen,int[] codes){
        if(id<0||id>=28||stageRoom!=null&&stageRoom.id==id)return false;
        return stage.register(id,leftScreen,topScreen,rightScreen,bottomScreen,codes);
    }
    @JSExport public static boolean registerStageRedirect(int id,int targetRoomId,int seedScreenX,int seedScreenY){
        StageMap.Room room=stage.get(id);if(room==null||targetRoomId<0||targetRoomId>=28||seedScreenX<0||seedScreenX>255||seedScreenY<0||seedScreenY>255||stageRoom==room)return false;
        room.redirectRoom=targetRoomId;room.redirectSeedX=seedScreenX;room.redirectSeedY=seedScreenY;return true;
    }
    @JSExport public static void clearStageRooms(){if(sceneMode==2)useLaboratory();stage.clear();enemies.clear();zombies.clear();candles.clear();clearDaggers();}
    @JSExport public static boolean registerBatDefinition(int[] initFields,int[] enemyFields,int[] flyPairs,int[] dropPairs){boolean accepted=enemies.registerDefinition(initFields,enemyFields,flyPairs,dropPairs);if(accepted){batHitboxWidth=enemyFields[4];batHitboxHeight=enemyFields[5];}return accepted;}
    @JSExport public static boolean registerStageBats(int roomId,int[] tuples){StageMap.Room room=stage.get(roomId);return room!=null&&room.redirectRoom<0&&enemies.registerLayout(roomId,room.width,room.height,tuples);}
    @JSExport public static String getEnemies(){String bats=enemies.getState(),floor=zombies.getState();return bats.length()==2?floor:floor.length()==2?bats:bats.substring(0,bats.length()-1)+','+floor.substring(1);}
    @JSExport public static String saveEnemies(){return enemies.save();}
    @JSExport public static boolean loadEnemies(String snapshot){return enemies.load(snapshot);}
    @JSExport public static boolean registerZombieDefinition(int[] initFields,int[] enemyFields,int[] walkPairs,int[] emergePairs,int[] sinkPairs,int[] fallSensors,int[] walkSensors){return zombies.registerDefinition(initFields,enemyFields,walkPairs,emergePairs,sinkPairs,fallSensors,walkSensors);}
    @JSExport public static boolean registerZombieSpawners(int roomId,int[] tuples){return zombies.registerLayouts(stage.get(roomId),tuples);}
    @JSExport public static void setCastleTurnedOn(boolean enabled){zombies.setCastleTurnedOn(enabled);}
    @JSExport public static String getZombieSpawners(){return zombies.getSpawners();}
    @JSExport public static boolean registerCandleDefinition(int[] initFields,int[] enemyFields,int[] animationPairs){return candles.registerCandleDefinition(initFields,enemyFields,animationPairs);}
    @JSExport public static boolean registerSmallHeartDefinition(int[] initFields,int[] enemyFields,int[] animationPairs){return candles.registerHeartDefinition(initFields,enemyFields,animationPairs);}
    @JSExport public static boolean registerCandleExplosionDefinition(int[] initFields,int[] enemyFields,int[] animationPairs,int velocityYFixed){return candles.registerExplosionDefinition(initFields,enemyFields,animationPairs,velocityYFixed);}
    @JSExport public static boolean registerStageCandles(int roomId,int[] tuples){return candles.registerLayouts(stage.get(roomId),tuples);}
    @JSExport public static void setCubeOfZoeActive(boolean enabled){candles.setCubeOfZoeActive(enabled);}
    @JSExport public static String getCandles(){return candles.getCandles();}
    @JSExport public static String getDrops(){return candles.getDrops();}
    @JSExport public static boolean registerPrizeDefinition(int itemId,int[] initFields,int[] enemyFields,int[] animationPairs,int prizeValue){return candles.registerPrizeDefinition(itemId,initFields,enemyFields,animationPairs,prizeValue);}
    @JSExport public static boolean registerPrizeCollisionSensors(int[] offsets){return candles.registerPrizeCollisionSensors(offsets);}
    @JSExport public static boolean registerSubweaponDefinition(int itemId,int[] initFields,int[] enemyFields,int[] animationPairs,int inventoryId){return candles.registerSubweaponDefinition(itemId,initFields,enemyFields,animationPairs,inventoryId);}
    @JSExport public static boolean registerSubweaponMappings(int[] prizeToInventory,int[] inventoryToPrize){return candles.registerSubweaponMappings(prizeToInventory,inventoryToPrize);}
    @JSExport public static boolean registerDaggerDefinition(int[] subweaponFields,int[] sineQuarter){return daggers.register(subweaponFields,sineQuarter);}
    @JSExport public static String getDaggers(){return daggers.getState();}
    @JSExport public static boolean registerAxeDefinition(int[] subweaponFields,int[] sineQuarter,int[] trailRgb){return axes.register(subweaponFields,sineQuarter,trailRgb);}
    @JSExport public static String getAxes(){return axes.getState();}
    @JSExport public static boolean registerHolyWaterDefinition(int[] bottleFields,int[] flameFields,int[] sineQuarter,int[] shardOffsets,int[] flameUvOrigins,int[] factoryBlueprints){return holyWater.register(bottleFields,flameFields,sineQuarter,shardOffsets,flameUvOrigins,factoryBlueprints);}
    @JSExport public static String getHolyWater(){return holyWater.getState();}
    @JSExport public static boolean registerHolyWaterParticles(int[] particleFields,int[] factoryBlueprint){return holyWater.registerParticles(particleFields,factoryBlueprint);}
    @JSExport public static boolean registerCrossDefinition(int[] parentFields13,int[] giantFields13,int[] sineQuarter1025,int[] packedRotMatrix4096,int[] factoryBlueprints18,int[] faceVertices552,int[] faceUvFlags230,int[] particleUV48,int[] paletteRampWords96){return crosses.register(parentFields13,giantFields13,sineQuarter1025,packedRotMatrix4096,factoryBlueprints18,faceVertices552,faceUvFlags230,particleUV48,paletteRampWords96);}
    @JSExport public static String getCrosses(){return crosses.getState();}
    @JSExport public static boolean registerBibleDefinition(int[] subweaponFields,int[] sineQuarter,int[] factoryBlueprints){return bibles.register(subweaponFields,sineQuarter,factoryBlueprints);}
    @JSExport public static String getBibles(){return bibles.getState();}
    @JSExport public static boolean registerStopwatchDefinition(int[] subweaponFields,int[] sineQuarter,int[] factoryBlueprints,int[] particleFields){return stopwatches.register(subweaponFields,sineQuarter,factoryBlueprints,particleFields);}
    @JSExport public static String getStopwatches(){return stopwatches.getState();}
    /** Explicit bounded test bridge, not a complete original cutscene system. */
    @JSExport public static void setStopwatchCutscenePause(boolean pause){stopwatches.setCutscenePause(pause);}
    @JSExport public static boolean registerReboundDefinition(int[] subweaponFields,int[] sineQuarter,int[] factoryBlueprints,int[] particleFields){return rebounds.register(subweaponFields,sineQuarter,factoryBlueprints,particleFields);}
    @JSExport public static String getRebounds(){return rebounds.getState();}
    @JSExport public static boolean registerVibhutiDefinition(int[] subweaponFields,int[] sineQuarter,int[] factoryBlueprint){return vibhuti.register(subweaponFields,sineQuarter,factoryBlueprint);}
    @JSExport public static String getVibhuti(){return vibhuti.getState();}
    @JSExport public static boolean registerAguneaDefinition(int[] subweaponFields,int[] sineQuarter,int[] factoryBlueprints,int[] atanTable){return agunea.register(subweaponFields,sineQuarter,factoryBlueprints,atanTable);}
    @JSExport public static String getAgunea(){return agunea.getState();}
    @JSExport public static boolean registerAguneaOutlineDefinition(int[] outlineProfile,int[] factoryBlueprint,int[] frameRecords){boolean accepted=playerOutlines.register(outlineProfile,factoryBlueprint,frameRecords);if(accepted)agunea.setOutlineSink(new AguneaWorld.OutlineSink(){public boolean registered(){return playerOutlines.registered();}public void request(int sequence){playerOutlines.request(sequence);}});return accepted;}
    @JSExport public static String getPlayerOutlines(){return playerOutlines.getState();}
    @JSExport public static boolean registerAguneaBlinkDefinition(int[] blinkProfile,int[] sineQuarter,int[] factoryBlueprint,int[] frameRecords){boolean accepted=playerBlinks.register(blinkProfile,sineQuarter,factoryBlueprint,frameRecords);if(accepted){playerOutlines.setBlinkImplemented(true);agunea.setBlinkSink(new AguneaWorld.BlinkSink(){public boolean registered(){return playerBlinks.registered();}public void request(int sequence,int px,int py,int facing){playerBlinks.request(sequence,px,py,facing);}});}return accepted;}
    @JSExport public static String getPlayerBlinks(){return playerBlinks.getState();}
    /** Volatile authored test control; complete accessory inventory is unported. */
    @JSExport public static boolean setAguneaHeartBroaches(int count){return agunea.setHeartBroaches(count);}
    /** Explicit authored durable target, never an original NO3 placement. */
    @JSExport public static boolean configureAguneaTestTarget(int px,int py,int health){if(solids==null||px<0||px>=worldWidth||py<0||py>=worldHeight||health<1||health>0x7000||agunea.activeCount()!=0)return false;aguneaTestTarget=new AguneaTestTarget(px,py,health);return true;}
    @JSExport public static boolean enterStageRoom(int id,int startX,int startY){
        StageMap.Room room=stage.get(id);
        if(room==null){navigationNotice="Room is not registered; external stage exits remain unfinished.";return false;}
        if(room.redirectRoom>=0){navigationNotice="Original internal redirect placement is unfinished; connector entry is blocked.";return false;}
        if(startX<0||startX>=room.width||startY<HEIGHT||startY>=room.height||!bodyClear(room,startX,startY,HEIGHT)){navigationNotice="Spawn is obstructed by original collision or unsupported shapes.";return false;}
        bindRoom(room);spawnX=startX;spawnY=startY;spawnRoomId=id;reset();return true;
    }
    @JSExport public static int[] getStageNeighbors(int id){return stage.neighbors(id);}
    private static void bindRoom(StageMap.Room room){
        sceneMode=2;stageRoom=room;originalTileCodes=true;tileSize=16;worldWidth=room.width;worldHeight=room.height;columns=room.columns;rows=room.rows;solids=room.codes;
        geometryFingerprint=room.fingerprint;unsupportedCollisionCount=room.unsupported;
        targets=new Target[0];enemies.enterRoom(room.id);zombies.reset(room);candles.reset(room);clearDaggers();
    }
    private static boolean roomPixelSolid(StageMap.Room room,int px,int py){
        StageMap.Room owner=stage.neighborAt(room,px,py);if(owner==null||owner.redirectRoom>=0)return true;
        return owner.blocks(px+(room.left-owner.left)*256,py+(room.top-owner.top)*256);
    }
    private static boolean bodyClear(StageMap.Room room,int px,int py,int height){
        for(int bodyY=py-height+1;bodyY<py;bodyY++)for(int bodyX=px-HALF_WIDTH+1;bodyX<px+HALF_WIDTH;bodyX++)if(roomPixelSolid(room,bodyX,bodyY))return false;
        return true;
    }
    private static boolean transitionIfNeeded(){
        if(sceneMode!=2||x>=0&&x<worldWidth*ONE&&y>=0&&y<worldHeight*ONE)return true;
        StageMap.Room previous=stageRoom,next=stage.neighborAt(previous,Math.floorDiv(x,ONE),Math.floorDiv(y,ONE));
        if(next==null||next==previous||next.redirectRoom>=0)return false;
        long nx=(long)x+(previous.left-next.left)*256L*ONE,ny=(long)y+(previous.top-next.top)*256L*ONE;
        if(nx<0||nx>=next.width*(long)ONE||ny<0||ny>=next.height*(long)ONE||!bodyClear(next,(int)(nx/ONE),(int)(ny/ONE),crouch?26:HEIGHT))return false;
        bindRoom(next);x=(int)nx;y=(int)ny;navigationNotice="";return true;
    }
    private static Target[] createTargets(){if(sceneMode==2)return new Target[0];Target[] list=new Target[4];for(int i=0;i<list.length;i++){int tx=Math.min(worldWidth-32,380+i*390);list[i]=new Target(tx,floorAt(tx)-12);}return list;}
    @JSExport public static void reset() {
        if(sceneMode==2&&stageRoom.id!=spawnRoomId)bindRoom(stage.get(spawnRoomId));
        x=spawnX*ONE;y=spawnY*ONE;vx=0;vy=0;face=1;animation=0;animationTick=0;
        tick=0;hp=100;hearts=0;hits=0;gold=0;candles.setEquippedSubweapon(0);attackTimer=0;attackSequence=0;invulnerable=0;clearDaggers();
        grounded=false;crouch=false;dead=false;accumulator=0;jumpWasDown=false;attackWasDown=false;
        navigationNotice="";
        checkpointX=spawnX;checkpointY=spawnY;checkpointRoomId=sceneMode==2?stageRoom.id:sceneMode==0?-1:-2;checkpointCrouch=false;
        for(int i=0;i<keys.length;i++)keys[i]=false;
        targets=createTargets();enemies.reset(sceneMode==2?stageRoom.id:-1);zombies.reset(sceneMode==2?stageRoom:null);candles.reset(sceneMode==2?stageRoom:null);
    }
    @JSExport public static void key(int code,boolean pressed){if(code>=0&&code<keys.length)keys[code]=pressed;}
    @JSExport public static void releaseKeys(){for(int i=0;i<keys.length;i++)keys[i]=false;}
    @JSExport public static void step(double seconds) {
        if(Double.isNaN(seconds)||Double.isInfinite(seconds)||seconds<0)return;
        accumulator+=Math.min(seconds,.25);
        while(accumulator+1e-10>=1.0/60){update();accumulator-=1.0/60;}
    }
    private static void update() {
        tick++;
        if(dead)return;
        if(invulnerable>0)invulnerable--;
        if(useSubweaponTimer>0)useSubweaponTimer--;
        if(throwPoseTimer>0)throwPoseTimer--;
        boolean left=keys[37]||keys[65],right=keys[39]||keys[68];
        boolean jump=keys[32]||keys[88],attack=keys[90]||keys[74];
        crouch=grounded&&(keys[40]||keys[83]||crouch&&!bodyClearAt(x/ONE,y/ONE,HEIGHT));
        if(attack&&!attackWasDown&&attackTimer==0){
            if(!(keys[38]||keys[87])||trySubweaponAttack()){attackTimer=20;attackSequence++;}
        }
        attackWasDown=attack;
        if(grounded&&jump&&!jumpWasDown&&!crouch){vy=JUMP;grounded=false;}
        if(!jump&&jumpWasDown&&vy<-ONE)vy=-ONE;
        jumpWasDown=jump;
        if(attackTimer==0&&!crouch&&left!=right){face=left?-1:1;vx=face*WALK;}
        else vx=approach(vx,0,grounded?GROUND_DRAG:AIR_DRAG);
        if(!grounded) {
            int gravity=GRAVITY;
            // Normal jump branch 0x11029 uses 22/128/5 near the held apex.
            if(jump&&vy>-(ONE/8)&&vy<3*ONE/8)gravity=GRAVITY/5;
            vy=Math.min(TERMINAL,vy+gravity);
        }
        moveX(vx);moveY(vy);
        int watchFailures=stopwatches.allocationFailures();
        // Original player-entity Stopwatch store precedes stage callbacks.
        // Advancing only this family here preserves that gate while retaining
        // the established local ordering of the other projectile families.
        stopwatches.update(x,y-24*ONE,face);
        if(sceneMode==2){enemies.update(x/ONE,y/ONE-24,cameraX(),cameraY(),stopwatches.timeStopped(),tick);zombies.update(x/ONE,y/ONE-24,cameraX(),cameraY(),tick,stopwatches.timeStopped());candles.markSubweaponPickups(x/ONE,y/ONE,crouch?26:HEIGHT,cameraX(),cameraY());int gained=candles.update(cameraX(),cameraY(),y/ONE,face,tick,stopwatches.timeStopped());if(gained>0)hearts=Math.min(HEART_TEST_MAX,hearts+gained);gold=Math.min(GOLD_MAX,gold+candles.goldCollected());candles.markPickups(x/ONE,y/ONE,crouch?26:HEIGHT);}
        int allocationFailures=daggers.allocationFailures()+axes.allocationFailures()+holyWater.allocationFailures()+crosses.allocationFailures()+bibles.allocationFailures()+rebounds.allocationFailures()+vibhuti.allocationFailures()+agunea.allocationFailures();
        holyWater.beginFrame();
        crosses.beginFrame();
        bibles.beginFrame();rebounds.beginFrame();
        agunea.beginFrame(x,y-24*ONE,face);
        playerOutlines.beginFrame(x,y-24*ONE,face);
        playerBlinks.beginFrame();
        for(int slot=0;slot<32;slot++){if(slot<16){daggers.updateSlot(slot,daggerEnvironment,cameraX(),cameraY());axes.updateSlot(slot,axeEnvironment,cameraX(),cameraY(),tick);}holyWater.updateSlot(slot,holyWaterEnvironment,cameraX(),cameraY(),tick);if(slot<16)crosses.updateSlot(slot,crossEnvironment,cameraX(),cameraY(),tick);bibles.updateSlot(slot,bibleEnvironment,x-cameraX()*ONE,y-(24+cameraY())*ONE,cameraX(),cameraY(),tick);rebounds.updateSlot(slot,reboundEnvironment,cameraX(),cameraY());if(slot<16)vibhuti.updateSlot(slot,vibhutiEnvironment,cameraX(),cameraY());agunea.updateSlot(slot,aguneaEnvironment,x,y-24*ONE,face,0,(keys[38]||keys[87])&&(keys[90]||keys[74]),cameraX(),cameraY());}
        if(daggers.allocationFailures()+axes.allocationFailures()+holyWater.allocationFailures()+crosses.allocationFailures()+bibles.allocationFailures()+rebounds.allocationFailures()+vibhuti.allocationFailures()+agunea.allocationFailures()!=allocationFailures||stopwatches.allocationFailures()!=watchFailures){subweaponAttackOutcome="primitive-allocation-failed";subweaponAttackNotice="Projectile allocation failed. The heart cost was already spent.";}
        if(attackTimer>0){
            if(attackTimer<=16&&attackTimer>=8)hitTargets();
            attackTimer--;
        }
        if(sceneMode==2&&invulnerable==0){int damage=Math.max(enemies.contactDamage(x/ONE,y/ONE,crouch?26:HEIGHT),zombies.contactDamage(x/ONE,y/ONE,crouch?26:HEIGHT));if(damage>0){hp=Math.max(0,hp-damage);invulnerable=60;if(hp==0)dead=true;}}
        for(Target t:targets)if(t.life==0&&t.respawn>0&&--t.respawn==0)t.life=3;
        int next=attackTimer>0?65:crouch?17:throwPoseTimer>0?throwAnimation:!grounded?(vy<0?22:27):Math.abs(vx)>ONE/4?7:0;
        if(next!=animation){animation=next;animationTick=0;}else animationTick++;
        // Selected cosmetics use the final current player pose. Their logical
        // reservation precedes this frame's pulses; full source inter-family
        // allocator/player/compositor phase remains a documented local bridge.
        PlayerOutlineWorld.Player outlinePlayer=new PlayerOutlineWorld.Player();outlinePlayer.x=x;outlinePlayer.y=y-24*ONE;outlinePlayer.face=face;outlinePlayer.frame=AnimationBank.frame(animation,animationTick);outlinePlayer.status=dead?0x40000:0;
        playerOutlines.update(outlinePlayer);
        PlayerBlinkWorld.Player blinkPlayer=new PlayerBlinkWorld.Player();blinkPlayer.x=x;blinkPlayer.y=y-24*ONE;blinkPlayer.face=face;blinkPlayer.frame=outlinePlayer.frame;blinkPlayer.status=outlinePlayer.status;
        playerBlinks.update(blinkPlayer,agunea.activeCount());
        if(y/ONE>worldHeight+64){hp=Math.max(0,hp-10);if(sceneMode==2&&checkpointRoomId!=stageRoom.id)bindRoom(stage.get(checkpointRoomId));x=checkpointX*ONE;y=checkpointY*ONE;vx=0;vy=0;crouch=checkpointCrouch;grounded=pixelSolid(checkpointX,checkpointY);invulnerable=90;if(hp==0)dead=true;}
    }
    private static void clearDaggers(){daggers.reset();axes.reset();holyWater.reset();crosses.reset();bibles.reset();stopwatches.reset();rebounds.reset();vibhuti.reset();agunea.reset();playerOutlines.reset();playerBlinks.reset();aguneaTestTarget=null;throwPoseTimer=useSubweaponTimer=0;subweaponAttackOutcome="none";subweaponAttackNotice="";}
    /** Returns true only when the original refusal permits normal-hand fallback. */
    private static boolean trySubweaponAttack(){
        int equipped=candles.equippedSubweapon();
        if(equipped==0){subweaponAttackOutcome="sword-fallback-no-subweapon";subweaponAttackNotice="No subweapon equipped. Sword attack.";return true;}
        if(equipped==2)return tryAxeAttack();
        if(equipped==3)return tryHolyWaterAttack();
        if(equipped==4)return tryCrossAttack();
        if(equipped==5)return tryBibleAttack();
        if(equipped==6)return tryStopwatchAttack();
        if(equipped==7)return tryReboundAttack();
        if(equipped==8)return tryVibhutiAttack();
        if(equipped==9)return tryAguneaAttack();
        if(equipped!=1){subweaponAttackOutcome="unsupported-subweapon";subweaponAttackNotice="This subweapon attack is still being rebuilt.";return false;}
        if(hearts<1){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="A dagger needs one heart. Sword attack.";return true;}
        if(!daggers.registered()){subweaponAttackOutcome="dagger-data-unregistered";subweaponAttackNotice="The original dagger definition and sine table are not registered.";return false;}
        if(daggers.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="Three daggers are already active. Sword attack.";return true;}
        if(!daggers.spawn(x,y-24*ONE,face,crouch,attackSequence+1)){subweaponAttackOutcome="dagger-data-unregistered";subweaponAttackNotice="Dagger could not be created from this local state.";return false;}
        hearts-=daggers.heartCost();attackSequence++;useSubweaponTimer=4;
        // This player's animation-state integration is authored. Original
        // Stand selects93/94; source Crouch/Walk/Jump/Fall do not set it here.
        throwAnimation=daggers.animation()+(grounded?0:1);throwPoseTimer=crouch?0:grounded?52:40;
        animation=throwAnimation;animationTick=-1; // selector advances this to0
        subweaponAttackOutcome="dagger-thrown";subweaponAttackNotice="Dagger thrown. One heart spent.";return false;
    }
    private static boolean tryAxeAttack(){
        if(hearts<4){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="An axe needs four hearts. Sword attack.";return true;}
        if(!axes.registered()){subweaponAttackOutcome="axe-data-unregistered";subweaponAttackNotice="The original axe definition and trail data are not registered.";return false;}
        if(axes.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="Two axes are already active, or the projectile pool is full. Sword attack.";return true;}
        if(!axes.spawn(x,y-24*ONE,face,crouch,attackSequence+1)){subweaponAttackOutcome="axe-data-unregistered";subweaponAttackNotice="Axe could not be created from this local state.";return false;}
        hearts-=axes.heartCost();attackSequence++;useSubweaponTimer=4;throwAnimation=93+(grounded?0:1);throwPoseTimer=crouch?0:grounded?52:40;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="axe-thrown";subweaponAttackNotice="Axe thrown. Four hearts spent.";return false;
    }
    private static boolean tryHolyWaterAttack(){
        if(hearts<3){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="Holy Water needs three hearts. Sword attack.";return true;}
        if(!holyWater.registered()){subweaponAttackOutcome="holy-water-data-unregistered";subweaponAttackNotice="The original Holy Water data is not registered.";return false;}
        if(holyWater.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="Three bottles are already active, or the projectile pool is full. Sword attack.";return true;}
        if(!holyWater.spawn(x,y-24*ONE,face,attackSequence+1)){subweaponAttackOutcome="holy-water-data-unregistered";subweaponAttackNotice="Holy Water could not be created from this local state.";return false;}
        hearts-=holyWater.heartCost();attackSequence++;useSubweaponTimer=4;
        // Selected source Stand/edge clip; complete player state still authored.
        throwAnimation=188+(grounded?0:1);throwPoseTimer=crouch?0:28;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="holy-water-thrown";subweaponAttackNotice="Holy Water thrown. Three hearts spent.";return false;
    }
    private static boolean tryCrossAttack(){
        if(hearts<100){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="Cross needs one hundred hearts. Sword attack.";return true;}
        if(!crosses.registered()){subweaponAttackOutcome="cross-data-unregistered";subweaponAttackNotice="The original Cross data is not registered.";return false;}
        if(crosses.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="A Cross is already active, or the projectile pool is full. Sword attack.";return true;}
        if(!crosses.spawn(x-cameraX()*ONE,y-(24+cameraY())*ONE,face,attackSequence+1)){subweaponAttackOutcome="cross-data-unregistered";subweaponAttackNotice="Cross could not be created from this local state.";return false;}
        hearts-=crosses.heartCost();attackSequence++;useSubweaponTimer=4;throwAnimation=220+(grounded?0:1);throwPoseTimer=crouch?0:48;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="cross-activated";subweaponAttackNotice="Cross activated. One hundred hearts spent.";return false;
    }
    private static boolean tryBibleAttack(){
        if(hearts<5){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="Bible needs five hearts. Sword attack.";return true;}
        if(!bibles.registered()){subweaponAttackOutcome="bible-data-unregistered";subweaponAttackNotice="The original Bible data is not registered.";return false;}
        if(bibles.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="A Bible is already active, or the projectile pool is full. Sword attack.";return true;}
        if(!bibles.spawn(x-cameraX()*ONE,y-(24+cameraY())*ONE,face,attackSequence+1)){subweaponAttackOutcome="bible-data-unregistered";subweaponAttackNotice="Bible could not be created from this local state.";return false;}
        hearts-=bibles.heartCost();attackSequence++;useSubweaponTimer=4;throwAnimation=93+(grounded?0:1);throwPoseTimer=crouch?0:grounded?52:40;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="bible-released";subweaponAttackNotice="Bible released. Five hearts spent.";return false;
    }
    private static boolean tryStopwatchAttack(){
        if(hearts<20){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="Stopwatch needs twenty hearts. Sword attack.";return true;}
        if(stopwatches.cutscenePause()){subweaponAttackOutcome="stopwatch-cutscene-inhibited";subweaponAttackNotice="Stopwatch is unavailable during this sequence.";return false;}
        if(!stopwatches.registered()){subweaponAttackOutcome="stopwatch-data-unregistered";subweaponAttackNotice="The original Stopwatch data is not registered.";return false;}
        if(stopwatches.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="A Stopwatch is already active, or the projectile pool is full. Sword attack.";return true;}
        if(!stopwatches.spawn(x,y-24*ONE,face,attackSequence+1)){subweaponAttackOutcome="stopwatch-data-unregistered";subweaponAttackNotice="Stopwatch could not be created from this local state.";return false;}
        hearts-=stopwatches.heartCost();attackSequence++;useSubweaponTimer=4;throwAnimation=184+(grounded?0:1);throwPoseTimer=crouch?0:48;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="stopwatch-activated";subweaponAttackNotice="Stopwatch activated. Twenty hearts spent.";return false;
    }
    private static boolean tryReboundAttack(){
        if(hearts<2){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="Rebound Stone needs two hearts. Sword attack.";return true;}
        if(!rebounds.registered()){subweaponAttackOutcome="rebound-data-unregistered";subweaponAttackNotice="The original Rebound Stone data is not registered.";return false;}
        if(rebounds.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="Three stones are already active, or the projectile pool is full. Sword attack.";return true;}
        if(!rebounds.spawn(x,y-24*ONE,face,attackSequence+1)){subweaponAttackOutcome="rebound-data-unregistered";subweaponAttackNotice="Rebound Stone could not be created from this local state.";return false;}
        hearts-=rebounds.heartCost();attackSequence++;useSubweaponTimer=4;throwAnimation=93+(grounded?0:1);throwPoseTimer=crouch?0:grounded?52:40;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="rebound-thrown";subweaponAttackNotice="Rebound Stone thrown. Two hearts spent.";return false;
    }
    private static boolean tryVibhutiAttack(){
        if(hearts<3){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="Vibhuti needs three hearts. Sword attack.";return true;}
        if(!vibhuti.registered()){subweaponAttackOutcome="vibhuti-data-unregistered";subweaponAttackNotice="The original Vibhuti data is not registered.";return false;}
        if(vibhuti.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="Two ash throws are already active, or the projectile pool is full. Sword attack.";return true;}
        if(!vibhuti.spawn(x,y-24*ONE,face,attackSequence+1)){subweaponAttackOutcome="vibhuti-data-unregistered";subweaponAttackNotice="Vibhuti could not be created from this local state.";return false;}
        hearts-=vibhuti.heartCost();attackSequence++;useSubweaponTimer=4;throwAnimation=188+(grounded?0:1);throwPoseTimer=crouch?0:28;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="vibhuti-thrown";subweaponAttackNotice="Vibhuti thrown. Three hearts spent.";return false;
    }
    private static int hitCrossObjects(CrossWorld.Actor actor,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){
        int count=0;
        if(sceneMode==2){
            for(int i=0;i<enemies.count();i++){EnemyWorld.Bat bat=enemies.bat(i);if(bat.active&&bat.alive&&!actor.hasHit(bat)&&Math.abs((bat.x>>16)-px)<batHitboxWidth+halfWidth&&Math.abs((bat.y>>16)-py)<batHitboxHeight+halfHeight&&actor.rememberHit(bat)){bat.lastHit=sequence;bat.hp=Math.max(0,bat.hp-damage);if(bat.hp==0)bat.alive=bat.active=false;count++;}}
            for(int i=0;i<8;i++){ZombieWorld.Zombie zombie=zombies.zombie(i);if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&!actor.hasHit(zombie)&&Math.abs((zombie.x>>16)-px)<zombie.hitboxWidth+halfWidth&&Math.abs((zombie.y>>16)+zombie.hitboxOffY-py)<zombie.hitboxHeight+halfHeight&&actor.rememberHit(zombie)){zombie.lastHit=sequence;zombie.hp=Math.max(0,zombie.hp-damage);if(zombie.hp==0){zombie.alive=zombie.active=false;zombie.hitboxState=0;}count++;}}
            for(int i=0;i<candles.count();i++){CandleWorld.Actor candle=candles.actor(i);if(candle.phase==CandleWorld.CANDLE&&candle.active&&candle.step>0&&!candle.hitPending&&!actor.hasHit(candle)&&Math.abs((candle.x>>16)-px)<candle.hitboxWidth+halfWidth&&Math.abs((candle.y>>16)-py)<candle.hitboxHeight+halfHeight&&actor.rememberHit(candle)){candle.hitPending=true;candle.lastHit=sequence;count++;}}
        }else for(Target target:targets)if(target.life>0&&!actor.hasHit(target)&&Math.abs(target.x-px)<12+halfWidth&&Math.abs(target.y-py)<16+halfHeight&&actor.rememberHit(target)){target.lastHit=sequence;target.life=Math.max(0,target.life-damage);if(target.life==0){hearts++;target.respawn=600;}count++;}
        hits+=count;return count>0?1:0;
    }
    private static int hitBibleObjects(BibleWorld.Actor actor,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){
        int count=0;
        if(sceneMode==2){
            for(int i=0;i<enemies.count();i++){EnemyWorld.Bat bat=enemies.bat(i);if(bat.active&&bat.alive&&!actor.hasHit(bat)&&Math.abs((bat.x>>16)-px)<batHitboxWidth+halfWidth&&Math.abs((bat.y>>16)-py)<batHitboxHeight+halfHeight&&actor.rememberHit(bat)){bat.lastHit=sequence;bat.hp=Math.max(0,bat.hp-damage);if(bat.hp==0)bat.alive=bat.active=false;count++;}}
            for(int i=0;i<8;i++){ZombieWorld.Zombie zombie=zombies.zombie(i);if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&!actor.hasHit(zombie)&&Math.abs((zombie.x>>16)-px)<zombie.hitboxWidth+halfWidth&&Math.abs((zombie.y>>16)+zombie.hitboxOffY-py)<zombie.hitboxHeight+halfHeight&&actor.rememberHit(zombie)){zombie.lastHit=sequence;zombie.hp=Math.max(0,zombie.hp-damage);if(zombie.hp==0){zombie.alive=zombie.active=false;zombie.hitboxState=0;}count++;}}
            for(int i=0;i<candles.count();i++){CandleWorld.Actor candle=candles.actor(i);if(candle.phase==CandleWorld.CANDLE&&candle.active&&candle.step>0&&!candle.hitPending&&!actor.hasHit(candle)&&Math.abs((candle.x>>16)-px)<candle.hitboxWidth+halfWidth&&Math.abs((candle.y>>16)-py)<candle.hitboxHeight+halfHeight&&actor.rememberHit(candle)){candle.hitPending=true;candle.lastHit=sequence;count++;}}
        }else for(Target target:targets)if(target.life>0&&!actor.hasHit(target)&&Math.abs(target.x-px)<12+halfWidth&&Math.abs(target.y-py)<16+halfHeight&&actor.rememberHit(target)){target.lastHit=sequence;target.life=Math.max(0,target.life-damage);if(target.life==0){hearts++;target.respawn=600;}count++;}
        hits+=count;return count>0?1:0;
    }
    private static int hitReboundObjects(ReboundWorld.Actor actor,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){
        int count=0;
        if(sceneMode==2){
            for(int i=0;i<enemies.count();i++){EnemyWorld.Bat bat=enemies.bat(i);if(bat.active&&bat.alive&&!actor.hasHit(bat)&&Math.abs((bat.x>>16)-px)<batHitboxWidth+halfWidth&&Math.abs((bat.y>>16)-py)<batHitboxHeight+halfHeight&&actor.rememberHit(bat)){bat.lastHit=sequence;bat.hp=Math.max(0,bat.hp-damage);if(bat.hp==0)bat.alive=bat.active=false;count++;}}
            for(int i=0;i<8;i++){ZombieWorld.Zombie zombie=zombies.zombie(i);if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&!actor.hasHit(zombie)&&Math.abs((zombie.x>>16)-px)<zombie.hitboxWidth+halfWidth&&Math.abs((zombie.y>>16)+zombie.hitboxOffY-py)<zombie.hitboxHeight+halfHeight&&actor.rememberHit(zombie)){zombie.lastHit=sequence;zombie.hp=Math.max(0,zombie.hp-damage);if(zombie.hp==0){zombie.alive=zombie.active=false;zombie.hitboxState=0;}count++;}}
            for(int i=0;i<candles.count();i++){CandleWorld.Actor candle=candles.actor(i);if(candle.phase==CandleWorld.CANDLE&&candle.active&&candle.step>0&&!candle.hitPending&&!actor.hasHit(candle)&&Math.abs((candle.x>>16)-px)<candle.hitboxWidth+halfWidth&&Math.abs((candle.y>>16)-py)<candle.hitboxHeight+halfHeight&&actor.rememberHit(candle)){candle.hitPending=true;candle.lastHit=sequence;count++;}}
        }else for(Target target:targets)if(target.life>0&&!actor.hasHit(target)&&Math.abs(target.x-px)<12+halfWidth&&Math.abs(target.y-py)<16+halfHeight&&actor.rememberHit(target)){target.lastHit=sequence;target.life=Math.max(0,target.life-damage);if(target.life==0){hearts++;target.respawn=600;}count++;}
        hits+=count;return count>0?1:0;
    }
    private static boolean aguneaOverlap(int tx,int ty,int tw,int th,int px,int py,int width,int height){return Math.abs(tx-px)<tw+width&&Math.abs(ty-py)<th+height;}
    private static AguneaWorld.Target captureAguneaTarget(int px,int py,int width,int height,int damage,int sequence){
        Object identity=null;
        if(aguneaTestTarget!=null&&aguneaTestTarget.hp>0&&aguneaOverlap(aguneaTestTarget.x,aguneaTestTarget.y,12,16,px,py,width,height))identity=aguneaTestTarget;
        if(identity==null&&sceneMode==2){
            for(int i=0;i<enemies.count()&&identity==null;i++){EnemyWorld.Bat bat=enemies.bat(i);if(bat.active&&bat.alive&&aguneaOverlap(bat.x>>16,bat.y>>16,batHitboxWidth,batHitboxHeight,px,py,width,height))identity=bat;}
            for(int i=0;i<8&&identity==null;i++){ZombieWorld.Zombie zombie=zombies.zombie(i);if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&aguneaOverlap(zombie.x>>16,(zombie.y>>16)+zombie.hitboxOffY,zombie.hitboxWidth,zombie.hitboxHeight,px,py,width,height))identity=zombie;}
            for(int i=0;i<candles.count()&&identity==null;i++){CandleWorld.Actor candle=candles.actor(i);if(candle.phase==CandleWorld.CANDLE&&candle.active&&candle.step>0&&!candle.hitPending&&aguneaOverlap(candle.x>>16,candle.y>>16,candle.hitboxWidth,candle.hitboxHeight,px,py,width,height))identity=candle;}
        }else if(identity==null)for(Target target:targets)if(target.life>0&&aguneaOverlap(target.x,target.y,12,16,px,py,width,height)){identity=target;break;}
        if(identity==null)return null;damageAguneaTarget(identity,damage,sequence);return resolveAguneaTarget(identity);
    }
    private static AguneaWorld.Target resolveAguneaTarget(Object identity){
        if(identity instanceof AguneaTestTarget){AguneaTestTarget t=(AguneaTestTarget)identity;return new AguneaWorld.Target(t,t==aguneaTestTarget?1:0,t.x*ONE,t.y*ONE,t.hp,t.hp==0?0x100:0);}
        if(identity instanceof EnemyWorld.Bat){EnemyWorld.Bat b=(EnemyWorld.Bat)identity;return new AguneaWorld.Target(b,72,b.x,b.y,b.hp,b.hp==0?0x100:0);}
        if(identity instanceof ZombieWorld.Zombie){ZombieWorld.Zombie z=(ZombieWorld.Zombie)identity;boolean exists=false;for(int i=0;i<8;i++)if(zombies.zombie(i)==z)exists=true;return new AguneaWorld.Target(z,exists?73:0,z.x,z.y,z.hp,z.hp==0?0x100:0);}
        if(identity instanceof CandleWorld.Actor){CandleWorld.Actor c=(CandleWorld.Actor)identity;return new AguneaWorld.Target(c,c.phase==CandleWorld.CANDLE?1:c.phase==CandleWorld.HEART?2:0,c.x,c.y,32767,0);}
        if(identity instanceof Target){Target t=(Target)identity;return new AguneaWorld.Target(t,1,t.x*ONE,t.y*ONE,t.life,t.life==0?0x100:0);}
        return null;
    }
    /** Authored base10 initial/once-per-pulse damage; child ribbons are visual. */
    private static void damageAguneaTarget(Object identity,int damage,int sequence){
        boolean applied=false;
        if(identity instanceof AguneaTestTarget){AguneaTestTarget t=(AguneaTestTarget)identity;if(t.hp>0){t.hp=Math.max(0,t.hp-damage);applied=true;}}
        else if(identity instanceof EnemyWorld.Bat){EnemyWorld.Bat b=(EnemyWorld.Bat)identity;if(b.hp>0){b.hp=Math.max(0,b.hp-damage);b.lastHit=sequence;if(b.hp==0)b.alive=b.active=false;applied=true;}}
        else if(identity instanceof ZombieWorld.Zombie){ZombieWorld.Zombie z=(ZombieWorld.Zombie)identity;if(z.hp>0){z.hp=Math.max(0,z.hp-damage);z.lastHit=sequence;if(z.hp==0){z.alive=z.active=false;z.hitboxState=0;}applied=true;}}
        else if(identity instanceof CandleWorld.Actor){CandleWorld.Actor c=(CandleWorld.Actor)identity;if(c.phase==CandleWorld.CANDLE&&c.active&&!c.hitPending){c.hitPending=true;c.lastHit=sequence;applied=true;}}
        else if(identity instanceof Target){Target t=(Target)identity;if(t.life>0){t.life=Math.max(0,t.life-damage);t.lastHit=sequence;if(t.life==0){hearts++;t.respawn=600;}applied=true;}}
        if(applied)hits++;
    }
    private static boolean tryAguneaAttack(){
        int cost=agunea.heartCost();if(hearts<cost){subweaponAttackOutcome="sword-fallback-insufficient-hearts";subweaponAttackNotice="Agunea needs more hearts. Sword attack.";return true;}
        if(!agunea.registered()){subweaponAttackOutcome="agunea-data-unregistered";subweaponAttackNotice="The original Agunea data is not registered.";return false;}
        if(agunea.refusal()!=0){subweaponAttackOutcome="sword-fallback-chain-limit";subweaponAttackNotice="Agunea is already active. Sword attack.";return true;}
        if(!agunea.spawn(x,y-24*ONE,face,attackSequence+1)){subweaponAttackOutcome="agunea-data-unregistered";subweaponAttackNotice="Agunea could not be created from this local state.";return false;}
        hearts-=cost;attackSequence++;useSubweaponTimer=4;throwAnimation=93+(grounded?0:1);throwPoseTimer=crouch?0:grounded?52:40;animation=throwAnimation;animationTick=-1;
        subweaponAttackOutcome="agunea-released";subweaponAttackNotice="Agunea released. Hold Up and attack to keep the lightning linked.";return false;
    }
    private static int hitVibhutiObjects(VibhutiWorld.Actor actor,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){
        int count=0;
        if(sceneMode==2){
            for(int i=0;i<enemies.count();i++){EnemyWorld.Bat bat=enemies.bat(i);if(bat.active&&bat.alive&&!actor.hasHit(bat)&&Math.abs((bat.x>>16)-px)<batHitboxWidth+halfWidth&&Math.abs((bat.y>>16)-py)<batHitboxHeight+halfHeight&&actor.rememberHit(bat)){bat.lastHit=sequence;bat.hp=Math.max(0,bat.hp-damage);if(bat.hp==0)bat.alive=bat.active=false;count++;}}
            for(int i=0;i<8;i++){ZombieWorld.Zombie zombie=zombies.zombie(i);if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&!actor.hasHit(zombie)&&Math.abs((zombie.x>>16)-px)<zombie.hitboxWidth+halfWidth&&Math.abs((zombie.y>>16)+zombie.hitboxOffY-py)<zombie.hitboxHeight+halfHeight&&actor.rememberHit(zombie)){zombie.lastHit=sequence;zombie.hp=Math.max(0,zombie.hp-damage);if(zombie.hp==0){zombie.alive=zombie.active=false;zombie.hitboxState=0;}count++;}}
            for(int i=0;i<candles.count();i++){CandleWorld.Actor candle=candles.actor(i);if(candle.phase==CandleWorld.CANDLE&&candle.active&&candle.step>0&&!candle.hitPending&&!actor.hasHit(candle)&&Math.abs((candle.x>>16)-px)<candle.hitboxWidth+halfWidth&&Math.abs((candle.y>>16)-py)<candle.hitboxHeight+halfHeight&&actor.rememberHit(candle)){candle.hitPending=true;candle.lastHit=sequence;count++;}}
        }else for(Target target:targets)if(target.life>0&&!actor.hasHit(target)&&Math.abs(target.x-px)<12+halfWidth&&Math.abs(target.y-py)<16+halfHeight&&actor.rememberHit(target)){target.lastHit=sequence;target.life=Math.max(0,target.life-damage);if(target.life==0){hearts++;target.respawn=600;}count++;}
        hits+=count;return count>0?1:0;
    }
    private static int hitHolyWaterObjects(HolyWaterWorld.Actor actor,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){
        int count=0;
        if(sceneMode==2){
            for(int i=0;i<enemies.count();i++){EnemyWorld.Bat bat=enemies.bat(i);if(bat.active&&bat.alive&&!actor.hasHit(bat)&&Math.abs((bat.x>>16)-px)<batHitboxWidth+halfWidth&&Math.abs((bat.y>>16)-py)<batHitboxHeight+halfHeight&&actor.rememberHit(bat)){bat.lastHit=sequence;bat.hp=Math.max(0,bat.hp-damage);if(bat.hp==0)bat.alive=bat.active=false;count++;}}
            for(int i=0;i<8;i++){ZombieWorld.Zombie zombie=zombies.zombie(i);if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&!actor.hasHit(zombie)&&Math.abs((zombie.x>>16)-px)<zombie.hitboxWidth+halfWidth&&Math.abs((zombie.y>>16)+zombie.hitboxOffY-py)<zombie.hitboxHeight+halfHeight&&actor.rememberHit(zombie)){zombie.lastHit=sequence;zombie.hp=Math.max(0,zombie.hp-damage);if(zombie.hp==0){zombie.alive=zombie.active=false;zombie.hitboxState=0;}count++;}}
            for(int i=0;i<candles.count();i++){CandleWorld.Actor candle=candles.actor(i);if(candle.phase==CandleWorld.CANDLE&&candle.active&&candle.step>0&&!candle.hitPending&&!actor.hasHit(candle)&&Math.abs((candle.x>>16)-px)<candle.hitboxWidth+halfWidth&&Math.abs((candle.y>>16)-py)<candle.hitboxHeight+halfHeight&&actor.rememberHit(candle)){candle.hitPending=true;candle.lastHit=sequence;count++;}}
        }else for(Target target:targets)if(target.life>0&&!actor.hasHit(target)&&Math.abs(target.x-px)<12+halfWidth&&Math.abs(target.y-py)<16+halfHeight&&actor.rememberHit(target)){target.lastHit=sequence;target.life=Math.max(0,target.life-damage);if(target.life==0){hearts++;target.respawn=600;}count++;}
        hits+=count;return count>0?1:0;
    }
    private static int hitAxeObjects(AxeWorld.Axe axe,int px,int py,int halfWidth,int halfHeight,int sequence,int damage){
        int count=0;
        if(sceneMode==2){
            for(int i=0;i<enemies.count();i++){EnemyWorld.Bat bat=enemies.bat(i);if(bat.active&&bat.alive&&!axe.hasHit(bat)&&Math.abs((bat.x>>16)-px)<batHitboxWidth+halfWidth&&Math.abs((bat.y>>16)-py)<batHitboxHeight+halfHeight&&axe.rememberHit(bat)){bat.lastHit=sequence;bat.hp=Math.max(0,bat.hp-damage);if(bat.hp==0)bat.alive=bat.active=false;count++;}}
            for(int i=0;i<8;i++){ZombieWorld.Zombie zombie=zombies.zombie(i);if(zombie!=null&&zombie.active&&zombie.hitboxState!=0&&zombie.hitboxHeight>0&&!axe.hasHit(zombie)&&Math.abs((zombie.x>>16)-px)<zombie.hitboxWidth+halfWidth&&Math.abs((zombie.y>>16)+zombie.hitboxOffY-py)<zombie.hitboxHeight+halfHeight&&axe.rememberHit(zombie)){zombie.lastHit=sequence;zombie.hp=Math.max(0,zombie.hp-damage);if(zombie.hp==0){zombie.alive=zombie.active=false;zombie.hitboxState=0;}count++;}}
            for(int i=0;i<candles.count();i++){CandleWorld.Actor candle=candles.actor(i);if(candle.phase==CandleWorld.CANDLE&&candle.active&&candle.step>0&&!candle.hitPending&&!axe.hasHit(candle)&&Math.abs((candle.x>>16)-px)<candle.hitboxWidth+halfWidth&&Math.abs((candle.y>>16)-py)<candle.hitboxHeight+halfHeight&&axe.rememberHit(candle)){candle.hitPending=true;candle.lastHit=sequence;count++;}}
        }else for(Target target:targets)if(target.life>0&&!axe.hasHit(target)&&Math.abs(target.x-px)<12+halfWidth&&Math.abs(target.y-py)<16+halfHeight&&axe.rememberHit(target)){target.lastHit=sequence;target.life=Math.max(0,target.life-damage);if(target.life==0){hearts++;target.respawn=600;}count++;}
        hits+=count;return count>0?1:0;
    }
    private static int hitDaggerObjects(int px,int py,int halfWidth,int halfHeight,int sequence,int damage){
        int count;
        if(sceneMode==2){
            count=enemies.hitSword(px,py,halfWidth,halfHeight,sequence,damage);
            if(count==0)count=zombies.hitSword(px,py,halfWidth,halfHeight,sequence,damage);
            if(count==0)count=candles.hitSword(px,py,halfWidth,halfHeight,sequence);
        }else{
            count=0;for(Target target:targets)if(target.life>0&&target.lastHit!=sequence&&Math.abs(target.x-px)<12+halfWidth&&Math.abs(target.y-py)<16+halfHeight){target.lastHit=sequence;target.life=Math.max(0,target.life-damage);count++;if(target.life==0){hearts++;target.respawn=600;}break;}
        }
        if(count!=0){hits+=count;return 1;}return 0;
    }
    private static int approach(int v,int target,int rate){return v<target?Math.min(target,v+rate):Math.max(target,v-rate);}
    private static boolean solid(int col,int row){if(col<0||col>=columns)return true;if(row<0||row>=rows)return false;int code=solids[row*columns+col];return originalTileCodes?(!TileCollider.isSupported(code)||TileCollider.isSolid(code,tileSize/2,tileSize/2)):code!=0;}
    private static boolean pixelSolid(int px,int py){if(sceneMode==2)return roomPixelSolid(stageRoom,px,py);if(px<0||px>=worldWidth)return true;if(py<0||py>=worldHeight)return false;int code=solids[(py/tileSize)*columns+px/tileSize];return originalTileCodes?(!TileCollider.isSupported(code)||TileCollider.isSolid(code,px%tileSize,py%tileSize)):code!=0;}
    private static boolean bodyClearAt(int px,int py,int height){for(int bodyY=py-height+1;bodyY<py;bodyY++)for(int bodyX=px-HALF_WIDTH+1;bodyX<px+HALF_WIDTH;bodyX++)if(pixelSolid(bodyX,bodyY))return false;return true;}
    private static int floorAt(int pixelX){int col=Math.max(0,Math.min(columns-1,pixelX/tileSize));for(int row=Math.max(HEIGHT/tileSize,worldHeight/(2*tileSize));row<rows;row++)if(solid(col,row))return row*tileSize;return worldHeight;}
    private static int nearbyGround(int px,int feetY){
        // Authored bounded foot correction; original recursive slope sensors
        // and their direction/form-specific response are still not ported.
        int first=sceneMode==2?feetY-8:Math.max(0,feetY-8),last=sceneMode==2?feetY+8:Math.min(worldHeight-1,feetY+8);
        for(int groundY=first;groundY<=last;groundY++)for(int footX=px-HALF_WIDTH+1;footX<px+HALF_WIDTH;footX++)if(pixelSolid(footX,groundY))return groundY;
        return Integer.MIN_VALUE;
    }
    private static void moveX(int delta) {
        int proposed=x+delta;
        if(originalTileCodes){
            int previousY=y;
            if(grounded&&proposed!=x){int foot=nearbyGround(proposed/ONE,y/ONE);if(foot!=Integer.MIN_VALUE&&bodyClearAt(proposed/ONE,foot,crouch?26:HEIGHT))y=foot*ONE;}
            int edge=proposed/ONE+(delta>=0?HALF_WIDTH:-HALF_WIDTH);
            if(!bodyClearAt(proposed/ONE,y/ONE,crouch?26:HEIGHT)){proposed=x;y=previousY;vx=0;}
            for(int py=y/ONE-(crouch?26:HEIGHT)+1;py<y/ONE-5;py++)if(pixelSolid(edge,py)){proposed=x;y=previousY;vx=0;break;}
            int previous=x;x=sceneMode==2?proposed:Math.max(HALF_WIDTH*ONE,Math.min((worldWidth-HALF_WIDTH)*ONE,proposed));if(!transitionIfNeeded()){x=previous;y=previousY;vx=0;}return;
        }
        int top=(y/ONE-(crouch?26:HEIGHT)+1)/tileSize,bottom=(y/ONE-1)/tileSize;
        if(delta>0){int col=(proposed/ONE+HALF_WIDTH)/tileSize;for(int row=Math.max(0,top);row<=bottom;row++)if(solid(col,row)){proposed=(col*tileSize-HALF_WIDTH-1)*ONE;vx=0;break;}}
        if(delta<0){int col=(proposed/ONE-HALF_WIDTH)/tileSize;for(int row=Math.max(0,top);row<=bottom;row++)if(solid(col,row)){proposed=((col+1)*tileSize+HALF_WIDTH)*ONE;vx=0;break;}}
        x=Math.max(HALF_WIDTH*ONE,Math.min((worldWidth-HALF_WIDTH)*ONE,proposed));
    }
    private static void moveY(int delta) {
        int previous=y,proposed=y+delta;
        if(originalTileCodes){
            grounded=false;
            int px=x/ONE;
            if(delta>=0){outer:for(int py=sceneMode==2?previous/ONE:Math.max(0,previous/ONE);py<=proposed/ONE;py++)for(int test=px-HALF_WIDTH+1;test<px+HALF_WIDTH;test++)if(pixelSolid(test,py)){proposed=py*ONE;vy=0;grounded=true;break outer;}}
            else {int height=crouch?26:HEIGHT;outer:for(int py=previous/ONE-height;py>=(sceneMode==2?proposed/ONE-height:Math.max(0,proposed/ONE-height));py--)for(int test=px-HALF_WIDTH+2;test<=px+HALF_WIDTH-2;test++)if(pixelSolid(test,py)){proposed=(py+1+height)*ONE;vy=0;break outer;}}
            y=proposed;if(!transitionIfNeeded()){y=previous;vy=0;}if(!grounded&&delta==0)vy=GRAVITY;return;
        }
        int left=(x/ONE-HALF_WIDTH+1)/tileSize,right=(x/ONE+HALF_WIDTH-1)/tileSize;
        grounded=false;
        if(delta>=0){
            int first=(previous/ONE)/tileSize,last=(proposed/ONE)/tileSize;
            outer:for(int row=Math.max(0,first);row<=last;row++)for(int col=left;col<=right;col++)if(solid(col,row)&&previous<=row*tileSize*ONE){proposed=row*tileSize*ONE;vy=0;grounded=true;break outer;}
        }else{
            int h=crouch?26:HEIGHT,first=(previous/ONE-h)/tileSize,last=(proposed/ONE-h)/tileSize;
            outer:for(int row=first;row>=Math.max(0,last);row--)for(int col=left;col<=right;col++)if(solid(col,row)){proposed=((row+1)*tileSize+h)*ONE;vy=0;break outer;}
        }
        y=proposed;
        if(!grounded&&delta==0)vy=GRAVITY;
    }
    private static void hitTargets() {
        int px=x/ONE,py=y/ONE;
        int swordCenter=px+face*27;
        for(Target t:targets)if(t.life>0&&t.lastHit!=attackSequence&&Math.abs(t.x-swordCenter)<27&&Math.abs(t.y-(py-22))<24){t.lastHit=attackSequence;t.life--;hits++;if(t.life==0){hearts++;t.respawn=600;}}
        if(sceneMode==2)hits+=enemies.hitSword(swordCenter,py-22,27,24,attackSequence,1)+zombies.hitSword(swordCenter,py-22,27,24,attackSequence,1)+candles.hitSword(swordCenter,py-22,27,24,attackSequence);
    }
    private static int roomId(){return sceneMode==2?stageRoom.id:sceneMode==0?-1:-2;}
    @JSExport public static void checkpoint(){checkpointX=x/ONE;checkpointY=y/ONE;checkpointRoomId=roomId();checkpointCrouch=crouch;}
    @JSExport public static String save() {
        // Position and statistics only; practice-target state is not serialized.
        return "JAVA-CHECKPOINT-4,"+sceneMode+","+roomId()+","+worldWidth+","+worldHeight+","+geometryFingerprint+","+x+","+y+","+hp+","+hearts+","+hits+","+checkpointRoomId+","+checkpointX+","+checkpointY+","+(crouch?1:0)+","+(checkpointCrouch?1:0)+","+gold+","+candles.equippedSubweapon();
    }
    @JSExport public static boolean load(String data) {
        if(data==null||data.length()>1024)return false;
        try {
            String[] p=data.split(",",-1);
            boolean legacy=p.length==16&&p[0].equals("JAVA-CHECKPOINT-2");
            boolean v3=p.length==17&&p[0].equals("JAVA-CHECKPOINT-3");
            boolean v4=p.length==18&&p[0].equals("JAVA-CHECKPOINT-4");
            if(!legacy&&!v3&&!v4)return false;
            int savedGold=legacy?0:Integer.parseInt(p[16]);if(savedGold<0||savedGold>GOLD_MAX)return false;
            int savedSubweapon=v4?Integer.parseInt(p[17]):0;if(savedSubweapon<0||savedSubweapon>9)return false;
            int mode=Integer.parseInt(p[1]),savedRoom=Integer.parseInt(p[2]),width=Integer.parseInt(p[3]),height=Integer.parseInt(p[4]),fingerprint=Integer.parseInt(p[5]);
            if(mode!=sceneMode)return false;
            StageMap.Room loadedRoom=mode==2?stage.get(savedRoom):null;
            int expectedWidth=loadedRoom==null?worldWidth:loadedRoom.width,expectedHeight=loadedRoom==null?worldHeight:loadedRoom.height;
            int expectedFingerprint=loadedRoom==null?geometryFingerprint:loadedRoom.fingerprint;
            if(mode==2&&(loadedRoom==null||loadedRoom.redirectRoom>=0)||mode!=2&&savedRoom!=roomId()||width!=expectedWidth||height!=expectedHeight||fingerprint!=expectedFingerprint)return false;
            int sx=Integer.parseInt(p[6]),sy=Integer.parseInt(p[7]),health=Integer.parseInt(p[8]);
            int sh=Integer.parseInt(p[9]),count=Integer.parseInt(p[10]),cpRoom=Integer.parseInt(p[11]),cx=Integer.parseInt(p[12]),cy=Integer.parseInt(p[13]);
            int savedCrouch=Integer.parseInt(p[14]),savedCheckpointCrouch=Integer.parseInt(p[15]);if(savedCrouch<0||savedCrouch>1||savedCheckpointCrouch<0||savedCheckpointCrouch>1)return false;
            int minX=mode==2?0:HALF_WIDTH*ONE,maxX=mode==2?width*ONE-1:(width-HALF_WIDTH)*ONE;
            StageMap.Room checkpointRoom=mode==2?stage.get(cpRoom):null;
            int cpWidth=checkpointRoom==null?width:checkpointRoom.width,cpHeight=checkpointRoom==null?height:checkpointRoom.height;
            if(mode==2&&(checkpointRoom==null||checkpointRoom.redirectRoom>=0)||mode!=2&&cpRoom!=savedRoom||sx<minX||sx>maxX||sy<0||sy>height*ONE||health<0||health>100||sh<0||sh>1000000||count<0||count>1000000||cx<0||cx>=cpWidth||cy<0||cy>=cpHeight)return false;
            if(mode==2&&(!bodyClear(loadedRoom,sx/ONE,sy/ONE,savedCrouch==1?26:HEIGHT)||!bodyClear(checkpointRoom,cx,cy,savedCheckpointCrouch==1?26:HEIGHT)))return false;
            if(mode!=2&&(!bodyClearAt(sx/ONE,sy/ONE,savedCrouch==1?26:HEIGHT)||!bodyClearAt(cx,cy,savedCheckpointCrouch==1?26:HEIGHT)))return false;
            if(mode==2)bindRoom(loadedRoom);
            x=sx;y=sy;hp=health;hearts=sh;hits=count;gold=savedGold;checkpointX=cx;checkpointY=cy;checkpointRoomId=cpRoom;
            vx=0;vy=0;attackTimer=0;invulnerable=0;dead=hp==0;grounded=pixelSolid(x/ONE,y/ONE);
            jumpWasDown=false;attackWasDown=false;accumulator=0;crouch=savedCrouch==1;checkpointCrouch=savedCheckpointCrouch==1;animation=crouch?17:0;animationTick=0;
            enemies.reset(sceneMode==2?stageRoom.id:-1);zombies.reset(sceneMode==2?stageRoom:null);candles.reset(sceneMode==2?stageRoom:null);candles.setEquippedSubweapon(savedSubweapon);clearDaggers();navigationNotice="";releaseKeys();return true;
        }catch(NumberFormatException ex){return false;}
    }
    @JSExport public static String getState() {
        int px=x/ONE,py=y/ONE;
        int camera=cameraX(),cameraY=cameraY();
        StringBuilder s=new StringBuilder(768);
        s.append("{\"x\":").append(px).append(",\"y\":").append(py).append(",\"xFixed\":").append(x).append(",\"yFixed\":").append(y).append(",\"vxFixed\":").append(vx).append(",\"vyFixed\":").append(vy);
        s.append(",\"camera\":").append(camera).append(",\"cameraY\":").append(cameraY).append(",\"roomId\":").append(roomId()).append(",\"stageRoom\":").append(sceneMode==2).append(",\"unsupportedCollisionCount\":").append(unsupportedCollisionCount).append(",\"roomBoundsScreens\":");
        if(stageRoom==null)s.append("null");else s.append('[').append(stageRoom.left).append(',').append(stageRoom.top).append(',').append(stageRoom.right).append(',').append(stageRoom.bottom).append(']');
        s.append(",\"navigationNotice\":\"").append(navigationNotice).append('"');
        s.append(",\"face\":").append(face).append(",\"animation\":").append(animation).append(",\"animationTick\":").append(animationTick).append(",\"tick\":").append(tick);
        s.append(",\"grounded\":").append(grounded).append(",\"crouch\":").append(crouch).append(",\"attack\":").append(attackTimer).append(",\"hp\":").append(hp).append(",\"hearts\":").append(hearts).append(",\"hits\":").append(hits).append(",\"gold\":").append(gold).append(",\"goldMax\":").append(GOLD_MAX).append(",\"equippedSubweapon\":").append(candles.equippedSubweapon()).append(",\"equippedSubweaponPrizeId\":").append(candles.equippedPrizeId()).append(",\"dead\":").append(dead);
        s.append(",\"worldWidth\":").append(worldWidth).append(",\"worldHeight\":").append(worldHeight).append(",\"checkpointRoomId\":").append(checkpointRoomId).append(",\"checkpointX\":").append(checkpointX).append(",\"checkpointY\":").append(checkpointY).append(",\"targets\":[");
        for(int i=0;i<targets.length;i++){if(i!=0)s.append(',');Target t=targets[i];s.append("{\"x\":").append(t.x).append(",\"y\":").append(t.y).append(",\"life\":").append(t.life).append('}');}
        return s.append("],\"enemies\":").append(getEnemies()).append(",\"castleTurnedOn\":").append(zombies.castleTurnedOn()).append(",\"zombieSpawners\":").append(zombies.getSpawners()).append(",\"cubeOfZoeActive\":").append(candles.cubeOfZoeActive()).append(",\"heartTestMax\":").append(HEART_TEST_MAX).append(",\"candles\":").append(candles.getCandles()).append(",\"drops\":").append(candles.getDrops()).append(",\"candleEffects\":").append(candles.getEffects()).append(",\"daggers\":").append(daggers.getState()).append(",\"axes\":").append(axes.getState()).append(",\"holyWater\":").append(holyWater.getState()).append(",\"crosses\":").append(crosses.getState()).append(",\"crossStats\":").append(crosses.getStatistics()).append(",\"bibles\":").append(bibles.getState()).append(",\"bibleStats\":").append(bibles.getStatistics()).append(",\"stopwatches\":").append(stopwatches.getState()).append(",\"stopwatchStats\":").append(stopwatches.getStatistics()).append(",\"rebounds\":").append(rebounds.getState()).append(",\"reboundStats\":").append(rebounds.getStatistics()).append(",\"vibhuti\":").append(vibhuti.getState()).append(",\"vibhutiStats\":").append(vibhuti.getStatistics()).append(",\"agunea\":").append(agunea.getState()).append(",\"playerOutlines\":").append(playerOutlines.getState()).append(",\"playerOutlineStats\":").append(playerOutlines.getStatistics()).append(",\"playerBlinks\":").append(playerBlinks.getState()).append(",\"playerBlinkStats\":").append(playerBlinks.getStatistics()).append(",\"aguneaStats\":").append(agunea.getStatistics()).append(",\"aguneaTestTarget\":").append(aguneaTestTarget==null?"{\"enabled\":false,\"authored\":true}":"{\"enabled\":true,\"authored\":true,\"x\":"+aguneaTestTarget.x+",\"y\":"+aguneaTestTarget.y+",\"hp\":"+aguneaTestTarget.hp+",\"initialHp\":"+aguneaTestTarget.initialHp+"}").append(",\"timeStopped\":").append(stopwatches.timeStopped()).append(",\"subweaponAttackOutcome\":\"").append(subweaponAttackOutcome).append("\",\"subweaponAttackNotice\":\"").append(subweaponAttackNotice).append("\",\"useSubweaponTimer\":").append(useSubweaponTimer).append('}').toString();
    }
    private static int cameraX(){return Math.max(0,Math.min(Math.max(0,worldWidth-VIEW_WIDTH),x/ONE-VIEW_WIDTH/2));}
    private static int cameraY(){return Math.max(0,Math.min(Math.max(0,worldHeight-VIEW_HEIGHT),y/ONE-VIEW_HEIGHT/2));}
    @JSExport public static int[] getCollision(){return solids.clone();}
    @JSExport public static int getTileSize(){return tileSize;}
    @JSExport public static boolean registerAnimation(int id,int[] durations,int[] frames,int loopStart){return AnimationBank.register(id,durations,frames,loopStart);}
    @JSExport public static int getFrame(){return AnimationBank.frame(animation,animationTick);}
    // Package-visible native Java checks use raw fixed-point values.
    static int positionX(){return x;} static int positionY(){return y;}
    static int velocityY(){return vy;} static int hitCount(){return hits;}
    static int attackTicks(){return attackTimer;}
    static boolean onGround(){return grounded;}
    static int currentRoomId(){return roomId();} static int velocityX(){return vx;} static int facing(){return face;}
    static StageMap.Room currentStageRoom(){return stageRoom;}
}
