package space.animefu.starfallhaven;

import java.util.*;
import static space.animefu.starfallhaven.ColonySystems.*;
import static space.animefu.starfallhaven.SocietySystems.*;

/** Deterministic, Android-independent simulation. All coordinates are tile units. */
public final class GameWorld {
    public static final int WIDTH=40, HEIGHT=28;
    public static final float DAY_SECONDS=90f, BOND_MAX=100f, BOND_DURATION=10f;
    private static final float BOND_NEARBY_RANGE2=3.25f*3.25f;
    public enum Tile { GRASS, WATER, TREE, ROCK, BERRY, CRYSTAL }
    public enum Tool { INSPECT, GATHER, WALL, BEACON }
    public enum Result { PLAYING, WON, LOST }
    public enum Event { GATHERED, BUILT, RAID, BOND_ACTIVATED }

    public static final class Colonist {
        public final String id,name;
        public AgeStage ageStage=AgeStage.ADULT;
        public int ageDays=24*YEAR_DAYS;
        public float x,y,health=100,hunger=100,energy=100,mood=72,comfort=50,safety=70;
        int tx=-1,ty=-1; float work;
        public boolean drafted,resting;
        public OrderType order=OrderType.NONE;
        public long attackTargetId=-1;
        public final EnumSet<Trait> traits=EnumSet.noneOf(Trait.class);
        public final EnumMap<Skill,Integer> skills=new EnumMap<>(Skill.class);
        public final EnumMap<WorkType,Integer> workPriorities=new EnumMap<>(WorkType.class);
        public final List<Thought> thoughts=new ArrayList<>();
        public final List<Injury> injuries=new ArrayList<>();
        long assignedObjectId=-1;
        WorkType assignedWork;

        Colonist(String name,float x,float y){this("legacy-"+name.toLowerCase(Locale.US),name,x,y);}
        Colonist(String id,String name,float x,float y){
            this.id=id;this.name=name;this.x=x;this.y=y;
            for(Skill s:Skill.values())skills.put(s,1);
            for(WorkType w:WorkType.values())workPriorities.put(w,w==WorkType.EMERGENCY?1:3);
        }
        public boolean alive(){return health>0;}
        public boolean adult(){return ageStage==AgeStage.ADULT&&ageDays>=18*YEAR_DAYS;}
        public boolean noncombatant(){return !adult();}
        public boolean hasJob(){return tx>=0;}
        public boolean isWorking(){return tx>=0&&Math.abs(x-(tx+.5f))<=.12f&&Math.abs(y-(ty+.5f))<=.12f;}
        public int targetX(){return tx;}
        public int targetY(){return ty;}
        public int skill(Skill value){Integer result=skills.get(value);return result==null?0:result;}
        public void setSkill(Skill value,int level){skills.put(value,clamp(level,0,10));}
        public int priority(WorkType value){Integer result=workPriorities.get(value);return result==null?0:result;}
        public void setPriority(WorkType value,int priority){workPriorities.put(value,clamp(priority,0,4));}
        public float efficiency(){float injury=0;for(Injury i:injuries)injury+=i.severity;return Math.max(.25f,1f-injury/140f)*(energy<20?.65f:1f);}
        public void addInjury(String label,float severity,float bleedPerSecond){injuries.add(new Injury(label,Math.max(0,severity),Math.max(0,bleedPerSecond)));}
        private static int clamp(int v,int lo,int hi){return Math.max(lo,Math.min(hi,v));}
    }
    public static final class Enemy {
        public final long id; public float x,y,health=42;
        Enemy(float x,float y){this(0,x,y);}
        Enemy(long id,float x,float y){this.id=id;this.x=x;this.y=y;}
    }
    static Enemy enemyForTest(float x,float y,float health){Enemy e=new Enemy(NEXT_TEST_ID--,x,y);e.health=health;return e;}
    private static long NEXT_TEST_ID=-1;

    /** Small explicit-state PRNG so a save continues deterministically after reload. */
    static final class Rng {
        long state; Rng(long seed){state=seed==0?0x9e3779b97f4a7c15L:seed;}
        long nextLong(){long x=state;x^=x<<13;x^=x>>>7;x^=x<<17;state=x;return x;}
        int nextInt(int bound){return (int)Long.remainderUnsigned(nextLong(),bound);}
        double nextDouble(){return (nextLong()>>>11)*0x1.0p-53;}
    }

    final Rng random;
    public final long worldSeed;
    public RegionalMap regionalMap;
    public FactionSimulation factionSimulation;
    public int selectedLandingX=-1,selectedLandingY=-1;
    public boolean landingSiteConfirmed;
    public final Tile[][] tiles=new Tile[WIDTH][HEIGHT];
    public final boolean[][] walls=new boolean[WIDTH][HEIGHT];
    public final List<Colonist> colonists=new ArrayList<>();
    public final List<Enemy> enemies=new ArrayList<>();
    public final List<Blueprint> blueprints=new ArrayList<>();
    public final List<StockpileZone> stockpiles=new ArrayList<>();
    public final List<ResourcePile> resourcePiles=new ArrayList<>();
    public final List<ProductionJob> productionJobs=new ArrayList<>();
    /** Serializable society state; save codecs can persist this independently or in a future save version. */
    public SocietyState society=new SocietyState();
    /** Persistence-ready authoritative incident record; intentionally not coupled to the current save codec. */
    public ChronicleHistory chronicleHistory=new ChronicleHistory();
    private final ChronicleDirector chronicleDirector=new ChronicleDirector();
    private final ArrayDeque<Event> events=new ArrayDeque<>();
    public int wood=12,stone=8,food=16,crystal=0,components=0,day=1;
    public int chronicleThreat,chronicleTension=20,chronicleNextDay=2;
    public float timeOfDay=.25f,beaconCharge=-1,messageTime,bondCharge,bondActiveSeconds;
    public boolean paused=false; public float speed=1; public Tool tool=Tool.GATHER; public Result result=Result.PLAYING;
    public String message="Tap a resource to assign a gathering job";
    boolean raidSpawned; long nextEntityId=1;

    public GameWorld(long seed){this(seed,true);}
    GameWorld(long seed,boolean initialize){
        worldSeed=seed;random=new Rng(seed);
        if(initialize){generate();initializeRegion(seed);addStarter("Mira",19.5f,13.5f,Trait.KINDLED_SPIRIT,Skill.MEDIC,5);addStarter("Taro",20.5f,13.5f,Trait.STEADY_HANDS,Skill.BUILD,6);addStarter("Sol",19.5f,14.5f,Trait.STAR_LISTENER,Skill.CRAFT,6);initializeSociety(seed);}
    }
    void initializeRegion(long seed){regionalMap=new RegionalMap(seed);RegionalMap.Cell best=regionalMap.bestLandingSite();selectedLandingX=best.x;selectedLandingY=best.y;factionSimulation=FactionSimulation.generate(seed,regionalMap,selectedLandingX,selectedLandingY);}
    void initializeSociety(long seed){
        society=new SocietyState();
        for(FactionSimulation.Faction nearby:factionSimulation.factions){Faction faction=new Faction(nearby.id,nearby.name,nearby.standing);society.factions.add(faction);if(nearby.relation()!=FactionSimulation.Relation.HOSTILE)society.caravans.add(Caravan.create("skiff-"+nearby.id,faction,seed,1));}
        for(Colonist c:colonists)society.families.add(new Person(c.id,c.name,AgeStage.ADULT,c.ageDays));
    }
    private void addStarter(String name,float x,float y,Trait trait,Skill specialty,int level){Colonist c=new Colonist(newColonistId(),name,x,y);c.traits.add(trait);c.setSkill(specialty,level);c.setSkill(Skill.HAUL,3);colonists.add(c);}
    /** Adds a young haven resident. Dependents are never eligible for jobs, drafting, or enemy targeting. */
    public synchronized Colonist addDependent(String name,AgeStage stage,int ageDays,float x,float y){if(stage==null||stage==AgeStage.ADULT)throw new IllegalArgumentException("dependent must be infant or child");Colonist c=new Colonist(newColonistId(),name,x,y);c.ageStage=stage;c.ageDays=Math.max(0,ageDays);c.drafted=false;colonists.add(c);return c;}
    private String newColonistId(){return "haven-"+(nextEntityId++);}
    long newId(){return nextEntityId++;}
    private void generate(){for(int x=0;x<WIDTH;x++)for(int y=0;y<HEIGHT;y++){double edge=Math.min(Math.min(x,WIDTH-1-x),Math.min(y,HEIGHT-1-y)),r=random.nextDouble();if(edge<1&&r<.45)tiles[x][y]=Tile.WATER;else if(r<.11)tiles[x][y]=Tile.TREE;else if(r<.18)tiles[x][y]=Tile.ROCK;else if(r<.225)tiles[x][y]=Tile.BERRY;else if(r<.25)tiles[x][y]=Tile.CRYSTAL;else tiles[x][y]=Tile.GRASS;}for(int x=16;x<=23;x++)for(int y=10;y<=17;y++)tiles[x][y]=Tile.GRASS;}

    public synchronized Colonist colonist(String id){for(Colonist c:colonists)if(c.id.equals(id))return c;return null;}
    public synchronized boolean selectLandingSite(int x,int y){if(landingSiteConfirmed||regionalMap==null||!regionalMap.inside(x,y)||!regionalMap.cell(x,y).canLand())return false;selectedLandingX=x;selectedLandingY=y;factionSimulation=FactionSimulation.generate(worldSeed,regionalMap,x,y);return true;}
    public synchronized boolean confirmLandingSite(){if(landingSiteConfirmed||selectedLandingX<0||regionalMap==null||!regionalMap.cell(selectedLandingX,selectedLandingY).canLand())return false;landingSiteConfirmed=true;return true;}
    public synchronized RegionalMap.Cell selectedLandingSite(){return regionalMap==null||selectedLandingX<0?null:regionalMap.cell(selectedLandingX,selectedLandingY);}
    public synchronized TradeReceipt buyTrade(String caravanId,int offerIndex,int quantity){
        Caravan caravan=null;for(Caravan c:society.caravans)if(c.id.equals(caravanId)){caravan=c;break;}if(caravan==null||offerIndex<0||offerIndex>=caravan.offers.size())return null;
        Inventory haven=new Inventory();haven.add(ResourceType.CRYSTAL,crystal);TradeOffer offer=caravan.offers.get(offerIndex);TradeReceipt receipt=caravan.buyFromCaravan(offer.id,quantity,haven,ResourceType.CRYSTAL);if(receipt.accepted){crystal=haven.amount(ResourceType.CRYSTAL);addResource(offer.resource,quantity);syncStanding(caravan.factionId);}return receipt;
    }
    public synchronized boolean offerAid(String factionId){Faction faction=societyFaction(factionId);if(faction==null||food<5)return false;food-=5;Diplomacy.offerAid(faction,5);syncStanding(factionId);return true;}
    public synchronized FamilyResult planChild(String firstId,String secondId){FamilyRegistry families=society.families;Person a=families.person(firstId),b=families.person(secondId);if(a==null||b==null)return new FamilyResult(false,"people unavailable",null);if(a.partnerId==null&&!families.formPartnership(firstId,secondId).accepted)return new FamilyResult(false,"partnership unavailable",null);return families.beginPregnancy(firstId,secondId,day+6);}
    private Faction societyFaction(String id){for(Faction f:society.factions)if(f.id.equals(id))return f;return null;}
    private void syncStanding(String id){Faction social=societyFaction(id);FactionSimulation.Faction regional=factionSimulation==null?null:factionSimulation.faction(id);if(social!=null&&regional!=null)regional.standing=social.reputation;}
    private void addResource(ResourceType type,int amount){if(type==ResourceType.WOOD)wood+=amount;else if(type==ResourceType.STONE)stone+=amount;else if(type==ResourceType.FOOD)food+=amount;else if(type==ResourceType.COMPONENT)components+=amount;else if(type==ResourceType.CRYSTAL)crystal+=amount;}
    /** Captures only stable values used by ChronicleDirector; no Android or network dependency crosses this boundary. */
    public synchronized ChronicleDirector.Snapshot chronicleSnapshot(){
        int living=0,known=0,friendly=0,hostile=0,best=-100;
        for(Colonist c:colonists)if(c.alive())living++;
        if(factionSimulation!=null)for(FactionSimulation.Faction faction:factionSimulation.factions){known++;best=Math.max(best,faction.standing);if(faction.relation()==FactionSimulation.Relation.FRIENDLY)friendly++;else if(faction.relation()==FactionSimulation.Relation.HOSTILE)hostile++;}
        return new ChronicleDirector.Snapshot(worldSeed,day,wood,stone,food,crystal,components,colonists.size(),living,known,friendly,hostile,best,chronicleThreat,chronicleTension);
    }
    /** Selects and applies one catalogued incident. Optional generated prose must never be passed back here. */
    public synchronized ChronicleDirector.Outcome runChronicle(){
        ChronicleDirector.Outcome outcome=chronicleDirector.select(chronicleSnapshot(),chronicleHistory);
        applyChronicleConsequence(outcome.consequence);
        String local=chronicleDirector.offlineNarrative(outcome);
        chronicleHistory.add(new ChronicleHistory.Entry(day,outcome.incident.id,outcome.selectionSeed,chronicleTension,outcome.incident.headline,local,"quiet","LOCAL"));
        chronicleNextDay=day+2+(int)Long.remainderUnsigned(ChronicleDirector.mix(outcome.selectionSeed),3);
        say(local);return outcome;
    }
    public synchronized boolean acceptChronicleNarrative(long selectionSeed,String title,String story,String tone){return chronicleHistory.replaceNarrative(selectionSeed,title,story,tone,"GROK");}
    public synchronized boolean acceptChronicleNarrative(int eventDay,String incidentId,long selectionSeed,String title,String story,String tone){return chronicleHistory.replaceNarrative(eventDay,incidentId,selectionSeed,title,story,tone,"GROK");}
    public synchronized ChronicleHistory.Entry latestChronicle(){return chronicleHistory.last();}
    public synchronized String offlineChronicleNarrative(ChronicleDirector.Outcome outcome){return chronicleDirector.offlineNarrative(outcome);}
    private void applyChronicleConsequence(ChronicleDirector.Consequence c){
        wood=Math.max(0,wood+c.woodDelta);stone=Math.max(0,stone+c.stoneDelta);food=Math.max(0,food+c.foodDelta);crystal=Math.max(0,crystal+c.crystalDelta);components=Math.max(0,components+c.componentDelta);
        chronicleThreat=ChronicleDirector.clamp(chronicleThreat+c.threatDelta,0,100);chronicleTension=ChronicleDirector.clamp(chronicleTension+c.tensionDelta,0,100);
        for(Colonist colonist:colonists)if(colonist.alive())colonist.mood=Math.max(0,Math.min(100,colonist.mood+c.moodDelta));
    }
    private void advanceSocietyDay(){
        List<FamilyEvent> events=society.families.advanceDay(day);for(FamilyEvent event:events)if(event.type==FamilyEvent.Type.BIRTH){Person child=society.families.person(event.subjectId);addDependent(child.name,AgeStage.INFANT,0,20.5f,14.5f);say(child.name+" welcomed safely into the haven");}
        for(Person person:society.families.people)if(person.stage!=AgeStage.ADULT){CareAssignment assignment=Caregiving.chooseCaregiver(society.families,person.id);if(assignment!=null)Caregiving.provideCare(society.families,assignment,12);}
        for(Iterator<Caravan> it=society.caravans.iterator();it.hasNext();)if(it.next().departureDay<day)it.remove();
        if(day%4==2&&!society.factions.isEmpty()){Faction faction=society.factions.get(Math.floorMod(day/4,society.factions.size()));if(faction.relation()!=RelationState.HOSTILE)society.caravans.add(Caravan.create("skiff-"+faction.id+"-"+day,faction,worldSeed^day,day));}
    }
    public synchronized boolean isNight(){return timeOfDay>=.68f||timeOfDay<.08f;}
    public synchronized void setTool(Tool t){tool=t;say(t==Tool.GATHER?"Gather: tap trees, rocks, berries or star crystal":t==Tool.WALL?"Wall: tap grass (cost 4 wood, 2 stone)":t==Tool.BEACON?"Haven Beacon: place on grass (30 wood, 20 stone, 12 crystal)":"Inspect colony");}
    public synchronized void command(int x,int y){
        if(result!=Result.PLAYING||!inside(x,y))return;
        if(tool==Tool.GATHER){if(!isResource(tiles[x][y])){say("Choose a tree, rock, berry bush or blue crystal");return;}Colonist c=nearestIdle(x+.5f,y+.5f);if(c==null){say("No healthy colonist available");return;}assign(c,x,y,WorkType.GATHER,-1);say(c.name+" assigned to gather");}
        else if(tool==Tool.WALL){if(tiles[x][y]!=Tile.GRASS||walls[x][y]){say("Walls need open grass");return;}if(wood<4||stone<2){say("Need 4 wood + 2 stone");return;}wood-=4;stone-=2;walls[x][y]=true;say("Barrier built");emit(Event.BUILT);}
        else if(tool==Tool.BEACON){if(beaconCharge>=0){say("The Haven Beacon is already active");return;}if(tiles[x][y]!=Tile.GRASS||walls[x][y]){say("Beacon needs open grass");return;}if(wood<30||stone<20||crystal<12){say("Beacon needs 30 wood, 20 stone, 12 crystal");return;}wood-=30;stone-=20;crystal-=12;beaconCharge=25;say("Beacon ignited! Defend it for 25 seconds");spawnRaid(7);emit(Event.BUILT);emit(Event.RAID);}
    }
    public synchronized boolean canGather(int x,int y){return inside(x,y)&&isResource(tiles[x][y]);}

    public synchronized boolean setDrafted(int colonistIndex,boolean drafted){return colonistIndex>=0&&colonistIndex<colonists.size()&&setDrafted(colonists.get(colonistIndex).id,drafted);}
    public synchronized boolean commandColonist(int colonistIndex,String command,int x,int y){if(colonistIndex<0||colonistIndex>=colonists.size())return false;Colonist c=colonists.get(colonistIndex);return "move".equalsIgnoreCase(command)?orderMove(c.id,x,y):"attack".equalsIgnoreCase(command)&&orderAttack(c.id,x,y);}
    public synchronized boolean setWorkPriority(int colonistIndex,int workIndex,int priority){if(colonistIndex<0||colonistIndex>=colonists.size())return false;WorkType[] ui={WorkType.CONSTRUCT,WorkType.GATHER,WorkType.CRAFT,WorkType.EMERGENCY};if(workIndex<0||workIndex>=ui.length)return false;colonists.get(colonistIndex).setPriority(ui[workIndex],priority);return true;}
    public synchronized boolean setDrafted(String id,boolean drafted){Colonist c=colonist(id);if(c==null||!c.alive()||(drafted&&c.noncombatant()))return false;c.drafted=drafted;clearJob(c);if(!drafted)c.order=OrderType.NONE;return true;}
    public synchronized boolean orderMove(String id,int x,int y){Colonist c=colonist(id);if(c==null||!c.drafted||blocked(x,y))return false;c.order=OrderType.MOVE;c.attackTargetId=-1;assign(c,x,y,null,-1);return true;}
    public synchronized boolean orderAttack(String id,long enemyId){Colonist c=colonist(id);Enemy e=enemy(enemyId);if(c==null||e==null||!c.drafted)return false;c.order=OrderType.ATTACK;c.attackTargetId=enemyId;assign(c,(int)e.x,(int)e.y,null,-1);return true;}
    public synchronized boolean orderAttack(String id,int x,int y){Enemy best=nearestEnemy(x+.5f,y+.5f,1.2f);return best!=null&&orderAttack(id,best.id);}
    public synchronized boolean orderMove(int colonistIndex,int x,int y){return colonistIndex>=0&&colonistIndex<colonists.size()&&orderMove(colonists.get(colonistIndex).id,x,y);}

    public synchronized Blueprint placeBlueprint(StructureType type,int x,int y){
        if(type==null||!inside(x,y)||tiles[x][y]!=Tile.GRASS||walls[x][y]||blueprintAt(x,y)!=null)return null;
        int wc=type==StructureType.WALL?4:type==StructureType.DOOR?5:type==StructureType.BED?8:12,sc=type==StructureType.WALL?2:type==StructureType.DOOR?1:type==StructureType.FABRICATOR?6:0,cc=type==StructureType.FABRICATOR?2:0;
        if(wood<wc||stone<sc||crystal<cc)return null;wood-=wc;stone-=sc;crystal-=cc;
        Blueprint b=new Blueprint(newId(),type,x,y,type==StructureType.WALL?3:type==StructureType.DOOR?4:type==StructureType.BED?5:8);blueprints.add(b);say(type+" blueprint designated");return b;
    }
    public synchronized boolean placeBlueprint(String type,int x,int y){
        if("storage".equalsIgnoreCase(type))return addStockpile(x,y,2,2,EnumSet.allOf(ResourceType.class))!=null;
        StructureType value;if("barrier".equalsIgnoreCase(type))value=StructureType.WALL;else if("door".equalsIgnoreCase(type))value=StructureType.DOOR;else if("bed".equalsIgnoreCase(type))value=StructureType.BED;else if("workbench".equalsIgnoreCase(type))value=StructureType.FABRICATOR;else return false;return placeBlueprint(value,x,y)!=null;
    }
    public synchronized StockpileZone addStockpile(int x,int y,int width,int height,EnumSet<ResourceType> allowed){if(width<=0||height<=0||x<0||y<0||x+width>WIDTH||y+height>HEIGHT)return null;StockpileZone z=new StockpileZone(newId(),x,y,width,height,allowed);stockpiles.add(z);return z;}
    public synchronized ResourcePile addResourcePile(ResourceType type,int amount,float x,float y){if(type==null||amount<=0)return null;ResourcePile p=new ResourcePile(newId(),type,amount,x,y);resourcePiles.add(p);return p;}
    public synchronized ProductionJob queueProduction(long benchId,Recipe recipe,int quantity){Blueprint b=blueprint(benchId);if(b==null||!b.complete||b.type!=StructureType.FABRICATOR||recipe==null||quantity<=0)return null;ProductionJob j=new ProductionJob(newId(),benchId,recipe,quantity);productionJobs.add(j);return j;}
    public synchronized boolean queueFirstFabricator(String recipe,int quantity){Recipe value;try{value=Recipe.valueOf(recipe.toUpperCase(Locale.US));}catch(Exception e){return false;}for(Blueprint b:blueprints)if(b.complete&&b.type==StructureType.FABRICATOR)return queueProduction(b.id,value,quantity)!=null;return false;}
    public synchronized List<Bed> beds(){List<Bed> out=new ArrayList<>();for(Blueprint b:blueprints)if(b.complete&&b.type==StructureType.BED)out.add(new Bed(b.id,b.x,b.y));return out;}

    public synchronized RoomStats roomAt(int sx,int sy){
        if(!inside(sx,sy)||walls[sx][sy])return new RoomStats(false,0,0,0);
        boolean[][] seen=new boolean[WIDTH][HEIGHT];ArrayDeque<int[]> q=new ArrayDeque<>();q.add(new int[]{sx,sy});seen[sx][sy]=true;int area=0,beds=0;boolean enclosed=true;
        while(!q.isEmpty()&&area<=WIDTH*HEIGHT){int[] p=q.remove();area++;if(blueprintTypeAt(p[0],p[1],StructureType.BED))beds++;int[][] ds={{1,0},{-1,0},{0,1},{0,-1}};for(int[] d:ds){int nx=p[0]+d[0],ny=p[1]+d[1];if(!inside(nx,ny)){enclosed=false;continue;}if(walls[nx][ny]||blueprintTypeAt(nx,ny,StructureType.DOOR))continue;if(!seen[nx][ny]){seen[nx][ny]=true;q.add(new int[]{nx,ny});}}}
        float comfort=Math.min(100,35+beds*18-Math.max(0,area-20)*.5f);return new RoomStats(enclosed,area,beds,comfort);
    }

    public synchronized void update(float rawDt){
        if(paused||result!=Result.PLAYING)return;float dt=Math.min(rawDt,.1f)*speed;
        float old=timeOfDay;timeOfDay+=dt/DAY_SECONDS;if(timeOfDay>=1){timeOfDay-=1;day++;raidSpawned=false;if(factionSimulation!=null)factionSimulation.advanceToDay(day);advanceSocietyDay();if(day>=chronicleNextDay)runChronicle();else say("Dawn of day "+day);}
        if(!raidSpawned&&timeOfDay>=.68f&&old<.68f){raidSpawned=true;spawnRaid(2+day);say("Nightfall raid! Colonists fight automatically");emit(Event.RAID);}
        boolean[] cooperating=new boolean[colonists.size()];for(int i=0;i<colonists.size();i++)for(int j=i+1;j<colonists.size();j++){Colonist a=colonists.get(i),b=colonists.get(j);if(a.alive()&&b.alive()&&dist2(a.x,a.y,b.x,b.y)<=BOND_NEARBY_RANGE2&&((a.tx>=0&&b.tx>=0)||(nearestEnemy(a.x,a.y,1.55f)!=null&&nearestEnemy(b.x,b.y,1.55f)!=null))){cooperating[i]=cooperating[j]=true;}}
        for(int i=0;i<colonists.size();i++)updateColonist(colonists.get(i),dt,cooperating[i]);updateEnemies(dt);if(bondActiveSeconds>0)bondActiveSeconds=Math.max(0,bondActiveSeconds-dt);
        if(beaconCharge>=0){beaconCharge-=dt;if(beaconCharge<=0){result=Result.WON;say("STARFALL HAVEN ENDURES — beacon rescue achieved!");}}
        boolean any=false;for(Colonist c:colonists)any|=c.alive();if(!any){result=Result.LOST;say("The haven has fallen. Tap RESTART.");}if(messageTime>0)messageTime-=dt;
    }
    private void updateColonist(Colonist c,float dt,boolean cooperating){
        if(!c.alive())return;
        c.hunger=Math.max(0,c.hunger-dt*.28f);c.energy=Math.max(0,c.energy-dt*(c.resting?-.75f:isNight()?.24f:.12f));if(c.hunger<=0||c.energy<=0)c.health-=dt*2.2f;
        updateHealthAndMood(c,dt);
        if(c.hunger<28&&food>0){food--;c.hunger=Math.min(100,c.hunger+38);addThought(c,"Shared stores",4,18);say(c.name+" ate from stores");}
        if(c.noncombatant()){c.drafted=false;c.order=OrderType.NONE;clearJob(c);c.resting=true;c.energy=Math.min(100,c.energy+dt*.45f);return;}
        if(c.drafted){c.resting=false;updateDrafted(c,dt,cooperating);return;}
        Enemy foe=nearestEnemy(c.x,c.y,1.55f);if(foe!=null){foe.health-=dt*13*(1+c.skill(Skill.COMBAT)*.035f)*teamCombatMultiplier();if(cooperating)chargeBond(dt*4.5f);return;}
        if(c.energy<22||c.resting){updateRest(c,dt);return;}
        if(c.tx>=0){moveOrWork(c,dt,cooperating);}else autoAssign(c);
    }
    private void updateHealthAndMood(Colonist c,float dt){
        float bleed=0;for(Iterator<Injury> it=c.injuries.iterator();it.hasNext();){Injury i=it.next();bleed+=i.bleedPerSecond;if(c.resting&&c.hunger>25){i.severity-=dt*(.22f+c.skill(Skill.MEDIC)*.025f);i.bleedPerSecond=Math.max(0,i.bleedPerSecond-dt*.02f);}if(i.severity<=0)it.remove();}c.health=Math.max(0,c.health-bleed*dt*.03f);
        for(Iterator<Thought> it=c.thoughts.iterator();it.hasNext();){Thought t=it.next();t.secondsLeft-=dt;if(t.secondsLeft<=0)it.remove();}
        float thoughts=0;for(Thought t:c.thoughts)thoughts+=t.moodEffect;c.safety=enemies.isEmpty()?75:35;c.comfort=c.resting?72:48;float target=20+c.hunger*.28f+c.energy*.25f+c.comfort*.12f+c.safety*.1f+thoughts;if(c.traits.contains(Trait.IRON_WILL))target+=7;c.mood=approach(c.mood,Math.max(0,Math.min(100,target)),dt*8);
        if(c.hunger<15)ensureThought(c,"Gnawing hunger",-14,3);if(c.energy<12)ensureThought(c,"Drifting awake",-10,3);if(!enemies.isEmpty())ensureThought(c,"Haven threatened",-8,3);
    }
    private void updateRest(Colonist c,float dt){
        Blueprint bed=nearestComplete(StructureType.BED,c.x,c.y);if(bed==null){c.resting=true;c.energy=Math.min(100,c.energy+dt*.32f);if(c.energy>80)c.resting=false;return;}
        float d=move(c,bed.x,bed.y,dt);if(d<=.12f){c.resting=true;c.energy=Math.min(100,c.energy+dt*.85f);if(c.energy>94){c.resting=false;clearJob(c);addThought(c,"Rested in a cradle",7,35);}}
    }
    private void updateDrafted(Colonist c,float dt,boolean cooperating){
        if(c.order==OrderType.ATTACK){Enemy target=enemy(c.attackTargetId);if(target==null){c.order=OrderType.NONE;clearJob(c);return;}float d2=dist2(c.x,c.y,target.x,target.y);if(d2<=1.55f*1.55f){target.health-=dt*15*(1+c.skill(Skill.COMBAT)*.04f)*teamCombatMultiplier();if(cooperating)chargeBond(dt*4.5f);}else move(c,(int)target.x,(int)target.y,dt);}
        else if(c.order==OrderType.MOVE){if(move(c,c.tx,c.ty,dt)<=.12f){c.order=OrderType.NONE;clearJob(c);}}
    }
    private void moveOrWork(Colonist c,float dt,boolean cooperating){
        if(move(c,c.tx,c.ty,dt)>.12f)return;c.work+=dt*teamWorkMultiplier()*c.efficiency()*skillMultiplier(c);if(cooperating)chargeBond(dt*3f);
        float required=c.assignedWork==WorkType.CONSTRUCT?construction(c.assignedObjectId).workRequired:c.assignedWork==WorkType.CRAFT?5f:c.assignedWork==WorkType.HAUL?.8f:2.1f;
        if(c.work<required)return;
        if(c.assignedWork==WorkType.CONSTRUCT)finishConstruction(c.assignedObjectId);else if(c.assignedWork==WorkType.CRAFT)finishProduction(c.assignedObjectId);else if(c.assignedWork==WorkType.HAUL)finishHaul(c,c.assignedObjectId);else harvest(c,c.tx,c.ty);
        clearJob(c);
    }
    private float skillMultiplier(Colonist c){Skill s=c.assignedWork==WorkType.CONSTRUCT?Skill.BUILD:c.assignedWork==WorkType.CRAFT?Skill.CRAFT:c.assignedWork==WorkType.HAUL?Skill.HAUL:Skill.GATHER;return 1+c.skill(s)*.05f;}
    private void autoAssign(Colonist c){
        if(c.priority(WorkType.CONSTRUCT)>0){Blueprint b=nearestIncomplete(c.x,c.y);if(b!=null){assign(c,b.x,b.y,WorkType.CONSTRUCT,b.id);return;}}
        if(c.priority(WorkType.HAUL)>0){ResourcePile p=nearestUnstored(c.x,c.y);if(p!=null){p.reservedBy=c.id;assign(c,(int)p.x,(int)p.y,WorkType.HAUL,p.id);return;}}
        if(c.priority(WorkType.CRAFT)>0){ProductionJob j=nextProduction();if(j!=null){Blueprint b=blueprint(j.benchId);assign(c,b.x,b.y,WorkType.CRAFT,j.id);return;}}
        if(c.priority(WorkType.GATHER)>0){Tile desired=food<7?Tile.BERRY:wood<30?Tile.TREE:stone<20?Tile.ROCK:crystal<12?Tile.CRYSTAL:null;if(desired!=null){int[] p=nearestTile(c,desired);if(p!=null)assign(c,p[0],p[1],WorkType.GATHER,-1);}}
    }
    private void finishConstruction(long id){Blueprint b=construction(id);if(b==null)return;b.workDone=b.workRequired;b.complete=true;if(b.type==StructureType.WALL)walls[b.x][b.y]=true;say(b.type+" completed");emit(Event.BUILT);}
    private void finishProduction(long id){ProductionJob j=production(id);if(j==null)return;j.workDone=0;j.remaining--;if(j.recipe==Recipe.NUTRIENT_PACK)food+=4;else components++;if(j.remaining<=0)productionJobs.remove(j);say("Fabrication cycle completed");emit(Event.BUILT);}
    private void finishHaul(Colonist c,long id){ResourcePile p=pile(id);if(p==null)return;StockpileZone z=nearestAccepting(p.type,p.x,p.y);if(z==null){p.reservedBy=null;return;}int[] spot=freeSpot(z);p.x=spot[0]+.5f;p.y=spot[1]+.5f;p.stored=true;p.reservedBy=null;c.x=p.x;c.y=p.y;say(c.name+" stored "+p.type.toString().toLowerCase(Locale.US));}
    private void harvest(Colonist c,int x,int y){switch(tiles[x][y]){case TREE:wood+=7;break;case ROCK:stone+=6;break;case BERRY:food+=8;break;case CRYSTAL:crystal+=4;break;default:return;}tiles[x][y]=Tile.GRASS;c.energy=Math.max(0,c.energy-5);say(c.name+" delivered resources");emit(Event.GATHERED);}
    private void updateEnemies(float dt){for(Iterator<Enemy>it=enemies.iterator();it.hasNext();){Enemy e=it.next();if(e.health<=0){it.remove();continue;}Colonist c=nearestAdult(e.x,e.y);if(c==null)continue;float dx=c.x-e.x,dy=c.y-e.y,d=(float)Math.sqrt(dx*dx+dy*dy);if(d<.72f){c.health-=dt*8*teamDamageTakenMultiplier();if(random.nextDouble()<dt*.05)c.addInjury("Void lash",8,1.5f);}else{float nx=e.x+dx/d*dt*.92f,ny=e.y+dy/d*dt*.92f;if(!blocked((int)nx,(int)ny)){e.x=nx;e.y=ny;}}}}

    public synchronized boolean activateBond(){if(bondCharge+.001f<BOND_MAX||bondActiveSeconds>0)return false;bondCharge=0;bondActiveSeconds=BOND_DURATION;say("RESONANCE BOND — united work, attack, and shielding!");emit(Event.BOND_ACTIVATED);return true;}
    public synchronized float teamWorkMultiplier(){return bondActiveSeconds>0?1.65f:1f;}
    public synchronized float teamCombatMultiplier(){return bondActiveSeconds>0?1.8f:1f;}
    public synchronized float teamDamageTakenMultiplier(){return bondActiveSeconds>0?.55f:1f;}
    private void chargeBond(float amount){if(bondActiveSeconds<=0)bondCharge=Math.min(BOND_MAX,bondCharge+amount);}
    synchronized void setBondForTest(float charge,float active){bondCharge=charge;bondActiveSeconds=active;}
    public synchronized List<Event> drainEvents(){List<Event> out=new ArrayList<>(events);events.clear();return out;}

    private void assign(Colonist c,int x,int y,WorkType work,long objectId){c.tx=x;c.ty=y;c.work=0;c.assignedWork=work;c.assignedObjectId=objectId;c.resting=false;}
    private void clearJob(Colonist c){c.tx=c.ty=-1;c.work=0;c.assignedWork=null;c.assignedObjectId=-1;}
    private float move(Colonist c,int x,int y,float dt){float gx=x+.5f,gy=y+.5f,dx=gx-c.x,dy=gy-c.y,d=(float)Math.sqrt(dx*dx+dy*dy);if(d>.12f){float step=Math.min(d,dt*1.65f*c.efficiency());float nx=c.x+dx/d*step,ny=c.y+dy/d*step;if(!blocked((int)nx,(int)ny)){c.x=nx;c.y=ny;}}return d;}
    private int[] nearestTile(Colonist c,Tile desired){int bx=-1,by=-1;float best=Float.MAX_VALUE;for(int x=0;x<WIDTH;x++)for(int y=0;y<HEIGHT;y++)if(tiles[x][y]==desired&&!targeted(x,y)){float d=dist2(c.x,c.y,x+.5f,y+.5f);if(d<best){best=d;bx=x;by=y;}}return bx<0?null:new int[]{bx,by};}
    private Blueprint nearestIncomplete(float x,float y){Blueprint out=null;float best=Float.MAX_VALUE;for(Blueprint b:blueprints)if(!b.complete&&!objectAssigned(b.id)){float d=dist2(x,y,b.x+.5f,b.y+.5f);if(d<best){best=d;out=b;}}return out;}
    private ResourcePile nearestUnstored(float x,float y){ResourcePile out=null;float best=Float.MAX_VALUE;for(ResourcePile p:resourcePiles)if(!p.stored&&p.reservedBy==null&&nearestAccepting(p.type,p.x,p.y)!=null){float d=dist2(x,y,p.x,p.y);if(d<best){best=d;out=p;}}return out;}
    private ProductionJob nextProduction(){for(ProductionJob j:productionJobs)if(!objectAssigned(j.id))return j;return null;}
    private Blueprint nearestComplete(StructureType type,float x,float y){Blueprint out=null;float best=Float.MAX_VALUE;for(Blueprint b:blueprints)if(b.complete&&b.type==type){float d=dist2(x,y,b.x+.5f,b.y+.5f);if(d<best){best=d;out=b;}}return out;}
    private StockpileZone nearestAccepting(ResourceType type,float x,float y){StockpileZone out=null;float best=Float.MAX_VALUE;for(StockpileZone z:stockpiles)if(z.accepts(type)){float d=dist2(x,y,z.x+.5f,z.y+.5f);if(d<best){best=d;out=z;}}return out;}
    private int[] freeSpot(StockpileZone z){for(int y=z.y;y<z.y+z.height;y++)for(int x=z.x;x<z.x+z.width;x++){boolean used=false;for(ResourcePile p:resourcePiles)if(p.stored&&(int)p.x==x&&(int)p.y==y){used=true;break;}if(!used)return new int[]{x,y};}return new int[]{z.x,z.y};}
    public synchronized Blueprint structureAt(int x,int y){return blueprintAt(x,y);}
    private Blueprint blueprintAt(int x,int y){for(Blueprint b:blueprints)if(b.x==x&&b.y==y)return b;return null;}
    private boolean blueprintTypeAt(int x,int y,StructureType type){Blueprint b=blueprintAt(x,y);return b!=null&&b.complete&&b.type==type;}
    private Blueprint blueprint(long id){for(Blueprint b:blueprints)if(b.id==id)return b;return null;}
    private Blueprint construction(long id){Blueprint b=blueprint(id);return b!=null&&!b.complete?b:null;}
    private ProductionJob production(long id){for(ProductionJob j:productionJobs)if(j.id==id)return j;return null;}
    private ResourcePile pile(long id){for(ResourcePile p:resourcePiles)if(p.id==id)return p;return null;}
    private Enemy enemy(long id){for(Enemy e:enemies)if(e.id==id)return e;return null;}
    private boolean objectAssigned(long id){for(Colonist c:colonists)if(c.assignedObjectId==id)return true;return false;}
    private boolean targeted(int x,int y){for(Colonist c:colonists)if(c.tx==x&&c.ty==y)return true;return false;}
    private void spawnRaid(int n){for(int i=0;i<n;i++){int side=random.nextInt(4);float x=side==0?1:side==1?WIDTH-2:random.nextInt(WIDTH-2)+1;float y=side==2?1:side==3?HEIGHT-2:random.nextInt(HEIGHT-2)+1;enemies.add(new Enemy(newId(),x+.5f,y+.5f));}}
    private Colonist nearestIdle(float x,float y){Colonist best=null;float bd=Float.MAX_VALUE;for(Colonist c:colonists)if(c.alive()&&c.adult()&&!c.drafted){float d=dist2(c.x,c.y,x,y)+(c.tx>=0?50:0);if(d<bd){bd=d;best=c;}}return best;}
    private Colonist nearestLiving(float x,float y){Colonist b=null;float bd=Float.MAX_VALUE;for(Colonist c:colonists)if(c.alive()){float d=dist2(x,y,c.x,c.y);if(d<bd){bd=d;b=c;}}return b;}
    private Colonist nearestAdult(float x,float y){Colonist b=null;float bd=Float.MAX_VALUE;for(Colonist c:colonists)if(c.alive()&&c.adult()){float d=dist2(x,y,c.x,c.y);if(d<bd){bd=d;b=c;}}return b;}
    private Enemy nearestEnemy(float x,float y,float range){Enemy b=null;float bd=range*range;for(Enemy e:enemies){float d=dist2(x,y,e.x,e.y);if(d<bd){bd=d;b=e;}}return b;}
    private boolean inside(int x,int y){return x>=0&&y>=0&&x<WIDTH&&y<HEIGHT;}
    private boolean blocked(int x,int y){return !inside(x,y)||tiles[x][y]==Tile.WATER||walls[x][y];}
    private boolean isResource(Tile t){return t==Tile.TREE||t==Tile.ROCK||t==Tile.BERRY||t==Tile.CRYSTAL;}
    private static float dist2(float x,float y,float a,float b){float dx=x-a,dy=y-b;return dx*dx+dy*dy;}
    private static float approach(float value,float target,float amount){return value<target?Math.min(target,value+amount):Math.max(target,value-amount);}
    private void addThought(Colonist c,String label,float effect,float duration){c.thoughts.add(new Thought(label,effect,duration));}
    private void ensureThought(Colonist c,String label,float effect,float duration){for(Thought t:c.thoughts)if(t.label.equals(label)){t.secondsLeft=duration;return;}addThought(c,label,effect,duration);}
    private void emit(Event e){events.add(e);} private void say(String s){message=s;messageTime=5;}
}
