import pyxel, math, random

# ══════════════════════════════════════════════════════════
#   SPACE PLATFORMER  v4  —  Ability Gate Edition
# ══════════════════════════════════════════════════════════
W, H      = 256, 144
GRAVITY   = 0.35
MAX_FALL  = 5.5
ARENA_W   = 2000

def clamp(v, lo, hi): return max(lo, min(hi, v))


# ── Partikel ─────────────────────────────────────────────
class Particle:
    def __init__(self, x, y, vx, vy, col, life):
        self.x, self.y, self.vx, self.vy = x, y, vx, vy
        self.col = col; self.life = self.max_life = life
    def update(self):
        self.x += self.vx; self.y += self.vy; self.vy += 0.06; self.life -= 1
    def draw(self):
        r = self.life / self.max_life
        col = self.col if r > 0.6 else (13 if r > 0.3 else 1)
        pyxel.pset(int(self.x), int(self.y), col)


# ── StarField ─────────────────────────────────────────────
class StarField:
    def __init__(self):
        self.layers = [
            [(random.randint(0, W*10), random.randint(16, H-1), sp) for _ in range(40)]
            for sp in [0.15, 0.4, 0.9]
        ]
        self.nebulas = [
            (random.randint(0, W*10), random.randint(16, H-20),
             random.choice([2, 5, 6]), random.randint(8, 20))
            for _ in range(12)
        ]
    def draw(self, cam_x):
        for nx, ny, nc, nr in self.nebulas:
            bx = int(nx - cam_x*0.08) % (W+80) - 40
            for dy in range(-nr//2, nr//2+1):
                for dx in range(-nr//2, nr//2+1):
                    if dx*dx+dy*dy < (nr//2)**2 and random.random() < 0.09:
                        pyxel.pset(clamp(bx+dx,0,W-1), clamp(ny+dy,16,H-1), nc)
        for i, layer in enumerate(self.layers):
            for sx, sy, sp in layer:
                pyxel.pset(int(sx - cam_x*sp*0.06) % W, sy, [7, 6, 13][i])


# ── VoidField ─────────────────────────────────────────────
class VoidField:
    def __init__(self):
        self.pts = [
            [random.randint(0,W), random.randint(16,H),
             random.uniform(-0.35,0.35), random.uniform(-1.0,-0.25),
             random.choice([2,8,14,1])]
            for _ in range(90)
        ]
        self.t = 0
    def update(self):
        self.t += 1
        for p in self.pts:
            p[0] += p[2]; p[1] += p[3]
            if p[1] < 16:
                p[0]=random.randint(0,W); p[1]=H+random.randint(0,20)
                p[2]=random.uniform(-0.35,0.35); p[3]=random.uniform(-1.0,-0.25)
                p[4]=random.choice([2,8,14,1])
    def draw(self, cam_x=0):
        t = self.t
        for p in self.pts:
            x=int(p[0]-cam_x*0.02)%W; y=int(p[1])
            if 16 <= y < H: pyxel.pset(x, y, p[4])
        cx = W//2
        for y in range(16, H, 2):
            off = int(math.sin(y*0.28+t*0.07)*3)
            pyxel.pset(clamp(cx+off,0,W-1), y, 2 if (t//12)%2==0 else 8)
        for i in range(4):
            r = 6+i*7+(t//6)%7
            for ang in range(0, 360, 14):
                rad=math.radians(ang+t*3)
                rx=int(W//2+r*math.cos(rad)); ry=int(H-3+r*0.28*math.sin(rad))
                if 0<=rx<W and ry<H: pyxel.pset(rx, ry, [2,8,14,1][i])


# ── Grapple Hook ──────────────────────────────────────────
class Grapple:
    HOOK_SPD=9; MAX_LEN=90
    def __init__(self):
        self.state="none"; self.hx=self.hy=0.0
        self.hvx=self.hvy=0.0; self.ax=self.ay=0.0
        self.rope_len=50.0; self.travel=0
    def fire(self, ox, oy, dx, dy):
        if self.state != "none": self.state="none"; return
        n=math.sqrt(dx*dx+dy*dy) or 1
        self.hvx=dx/n*self.HOOK_SPD; self.hvy=dy/n*self.HOOK_SPD
        self.hx,self.hy=ox,oy; self.travel=0; self.state="flying"
    def release(self): self.state="none"
    def update(self, px, py, platforms):
        if self.state!="flying": return
        self.hx+=self.hvx; self.hy+=self.hvy; self.travel+=1
        if self.hy<0 or self.hx<-50 or self.hx>10000 or self.hy>H or self.travel>30:
            self.state="none"; return
        for platx,platy,platw,plath in platforms:
            if platx<=self.hx<=platx+platw and platy<=self.hy<=platy+plath:
                self.ax,self.ay=self.hx,platy
                dx2=px-self.ax; dy2=py-self.ay
                self.rope_len=clamp(math.sqrt(dx2*dx2+dy2*dy2),18,self.MAX_LEN)
                self.state="attached"; return
    def apply(self, player):
        if self.state!="attached": return
        dx=player.x+4-self.ax; dy=player.y+4-self.ay
        dist=math.sqrt(dx*dx+dy*dy) or 1
        if dist>self.rope_len:
            ratio=self.rope_len/dist
            player.x=self.ax+dx*ratio-4; player.y=self.ay+dy*ratio-4
            nx,ny=dx/dist,dy/dist; dot=player.vx*nx+player.vy*ny
            if dot>0: player.vx-=dot*nx*0.88; player.vy-=dot*ny*0.88
    def draw(self, px, py, cam_x):
        pcx,pcy=int(px+4-cam_x),int(py+4)
        if self.state=="flying":
            hsx=int(self.hx-cam_x)
            pyxel.line(pcx,pcy,hsx,int(self.hy),6); pyxel.circ(hsx,int(self.hy),1,11)
        elif self.state=="attached":
            asx,asy=int(self.ax-cam_x),int(self.ay)
            mx=(pcx+asx)//2; my=(pcy+asy)//2+5; prev=(pcx,pcy)
            for i in range(1,13):
                t=i/12
                rx=int(pcx*(1-t)**2+mx*2*t*(1-t)+asx*t**2)
                ry=int(pcy*(1-t)**2+my*2*t*(1-t)+asy*t**2)
                pyxel.line(prev[0],prev[1],rx,ry,11 if i%2==0 else 6); prev=(rx,ry)
            pyxel.rect(asx-1,asy-1,3,3,7)


# ── Spieler ───────────────────────────────────────────────
class Player:
    SIZE=8
    def __init__(self, x, y):
        self.x,self.y=float(x),float(y); self.vx=self.vy=0.0
        self.on_ground=False; self.on_wall=0; self.wall_slide=False
        self.facing=1; self.hp=self.max_hp=6; self.invincible=0
        self.coyote=0; self.jump_buf=0; self.anim=0; self.trail=[]
        self.dead=False; self.dash_cd=0; self.dash_timer=0
        self.grapple=Grapple()
        self.last_wall=0   # which wall was last jumped from (0=none, 1=right, -1=left)
    def damage(self, amt=1):
        if self.invincible>0: return
        self.hp-=amt; self.invincible=75
        pyxel.play(3,5)
        if self.hp<=0: self.dead=True
    def heal(self, amt=1): self.hp=min(self.max_hp,self.hp+amt)
    def do_jump(self, wall=0):
        if wall: self.vy=-4.6; self.vx=-wall*3.0
        else: self.vy=-4.5
        self.coyote=self.jump_buf=0
        pyxel.play(3,0)
        for _ in range(8):
            self.trail.append(Particle(self.x+4,self.y+8,
                random.uniform(-1.2,1.2),random.uniform(0.8,3.2),
                random.choice([10,9,8]),random.randint(8,16)))
    def do_dash(self):
        if self.dash_cd>0 or self.grapple.state!="none": return
        self.dash_timer=10; self.dash_cd=55; self.vx=self.facing*6.5
        self.invincible=max(self.invincible,18)
        pyxel.play(3,6)
        for _ in range(12):
            self.trail.append(Particle(self.x+4,self.y+4,
                -self.facing*random.uniform(1,3.5),random.uniform(-0.6,0.6),
                random.choice([11,3,7,6]),random.randint(6,13)))
    def update(self, platforms, moving_platforms, spikes, has_floor=False):
        k=pyxel
        if k.btnp(k.KEY_S): self.do_dash()
        if k.btnp(k.KEY_C):
            fdx=self.facing*0.42; fdy=-1.0
            fn=math.sqrt(fdx*fdx+fdy*fdy)
            self.grapple.fire(self.x+4,self.y+4,fdx/fn,fdy/fn)
        if self.dash_timer<=0:
            acc=0.36
            if k.btn(k.KEY_LEFT): self.vx-=acc; self.facing=-1
            elif k.btn(k.KEY_RIGHT): self.vx+=acc; self.facing=1
            else: self.vx*=0.70
            self.vx=clamp(self.vx,-2.0,2.0)
        else: self.dash_timer-=1; self.vx*=0.88
        if k.btnp(k.KEY_UP) or k.btnp(k.KEY_Z): self.jump_buf=10
        if self.on_ground: self.coyote=8
        elif not self.wall_slide: self.coyote-=1
        self.jump_buf-=1
        if self.jump_buf>0 and self.coyote>0 and not self.wall_slide: self.do_jump()
        # Wall-jump: press jump + horizontal key; cannot re-use same wall
        if self.wall_slide and (k.btnp(k.KEY_UP) or k.btnp(k.KEY_Z)):
            if k.btn(k.KEY_LEFT) or k.btn(k.KEY_RIGHT):
                self.last_wall = self.on_wall      # mark wall used
                self.do_jump(wall=self.on_wall)
        if (k.btnr(k.KEY_UP) or k.btnr(k.KEY_Z)) and self.vy<-2: self.vy=-2.0
        if self.grapple.state=="attached" and (k.btnp(k.KEY_UP) or k.btnp(k.KEY_Z)):
            # Tangent boost — push in the direction the player is already swinging
            rdx = self.x+4 - self.grapple.ax
            rdy = self.y+4 - self.grapple.ay
            rlen = math.sqrt(rdx*rdx+rdy*rdy) or 1
            # Two perpendicular tangents; pick the one matching current velocity
            tx, ty = -rdy/rlen, rdx/rlen
            if tx*self.vx + ty*self.vy < 0:
                tx, ty = -tx, -ty
            boost = 3.8
            self.vx = clamp(self.vx + tx*boost, -5.5, 5.5)
            self.vy = clamp(self.vy + ty*boost, -6.0, 4.0)
            self.grapple.release()
        # Wall-slide: moderate descent (cap at 1.0 px/frame downward)
        if self.wall_slide:
            if self.vy > 1.0: self.vy = 1.0   # cap falling speed
            else: self.vy = min(self.vy + GRAVITY * 0.16, 1.0)  # slide down
        elif self.grapple.state=="attached": self.vy=clamp(self.vy+GRAVITY,-9,9)
        else: self.vy=min(self.vy+GRAVITY,MAX_FALL)
        self.x+=self.vx; self.y+=self.vy
        self.grapple.update(self.x+4,self.y+4,platforms)
        self.grapple.apply(self)
        self.on_ground=False; self.on_wall=0; self.wall_slide=False
        if has_floor and self.y+self.SIZE>=H:
            self.y=H-self.SIZE; self.vy=0; self.on_ground=True; self.grapple.release()
        prev_bot = self.y + self.SIZE - self.vy   # bottom position last frame
        prev_top = self.y - self.vy                  # top position last frame
        for px2,py2,pw,ph in platforms:
            if self.x<px2+pw and self.x+self.SIZE>px2:
                # Top collision: only if player was above platform top last frame
                if self.vy>=0 and self.y+self.SIZE>=py2 and prev_bot<=py2+4:
                    self.y=py2-self.SIZE; self.vy=0; self.on_ground=True; self.grapple.release()
                # Ceiling collision: player jumps into the bottom of a platform
                plat_bot = py2 + ph
                if self.vy < -0.3 and self.y < plat_bot and prev_top >= plat_bot - 1:
                    self.y = plat_bot    # pushed back below ceiling
                    self.vy = 0
            # Inner-wall detection: player inside shaft presses against inner face.
            if self.y+self.SIZE > py2+2 and self.y < py2+ph:
                # Left wall inner face: player right side near px2+pw
                if self.x >= px2+pw-3 and self.x <= px2+pw+2:
                    self.on_wall = -1
                # Right wall inner face: player left side near px2
                if self.x+self.SIZE >= px2-2 and self.x+self.SIZE <= px2+3:
                    self.on_wall = 1
                # Solid: block passing through left face
                if self.vx < 0 and self.x <= px2+pw and self.x >= px2+pw-6:
                    self.x = float(px2+pw); self.vx = 0
                # Solid: block passing through right face
                if self.vx > 0 and self.x+self.SIZE >= px2 and self.x+self.SIZE <= px2+6:
                    self.x = float(px2 - self.SIZE); self.vx = 0
        # Cling on inner wall (different from last jumped)
        if self.on_wall != 0 and not self.on_ground:
            if self.on_wall != self.last_wall:
                self.wall_slide = True
                self.vy = 0   # perfect cling — no gravity
        if self.on_ground:
            self.last_wall = 0
        for mp in moving_platforms:
            mx,my,mw=mp["x"],mp["y"],mp.get("w",24)
            if self.x<mx+mw and self.x+self.SIZE>mx:
                if self.vy>=0 and self.y+self.SIZE>=my and prev_bot<=my+4:
                    self.y=my-self.SIZE; self.vy=0; self.on_ground=True; self.x+=mp["vx"]
        for sx2,sy2 in spikes:
            if self.x<sx2+4 and self.x+self.SIZE>sx2 and self.y<sy2+4 and self.y+self.SIZE>sy2:
                self.damage(2)
        self.x=max(0,self.x)
        if self.invincible>0: self.invincible-=1
        if self.dash_cd>0: self.dash_cd-=1
        self.anim=(self.anim+1)%20
        if abs(self.vx)>0.5 and self.on_ground:
            self.trail.append(Particle(self.x+4,self.y+7,
                random.uniform(-0.4,0.4),random.uniform(0.1,0.5),random.choice([1,2,5]),random.randint(3,7)))
        if self.wall_slide:
            wx=self.x+8 if self.on_wall==1 else self.x
            self.trail.append(Particle(wx,self.y+self.SIZE//2+random.randint(-2,2),
                random.uniform(-0.8,0.8),random.uniform(0.3,1.2),random.choice([10,9,7]),random.randint(4,9)))
        self.trail=[p for p in self.trail if p.life>0]
        for p in self.trail: p.update()
    def draw(self, cam_x, void_mode=False):
        sx=int(self.x-cam_x); sy=int(self.y)
        self.grapple.draw(self.x,self.y,cam_x)
        for p in self.trail: p.draw()
        if self.invincible>0 and (self.invincible//4)%2==0: return
        if self.dash_timer>0: pyxel.rect(sx-1,sy-1,10,10,14 if void_mode else 11)
        suit=13 if void_mode else 6
        pyxel.rect(sx+2,sy+3,4,4,suit)    # body
        pyxel.rect(sx+1,sy,6,3,7)          # helmet
        pyxel.rect(sx+2,sy+1,4,1,12)       # visor
        pyxel.rect(sx+2,sy+7,2,1,5)        # left leg
        pyxel.rect(sx+5,sy+7,2,1,5)        # right leg
        arm=1 if self.anim<10 else 0
        if self.facing==1: pyxel.pset(sx+6,sy+3+arm,suit); pyxel.pset(sx+1,sy+4-arm,suit)
        else: pyxel.pset(sx+1,sy+3+arm,suit); pyxel.pset(sx+6,sy+4-arm,suit)
        if not self.on_ground and self.vy<-1:
            pyxel.pset(sx+3,sy+8,10); pyxel.pset(sx+4,sy+8,9)
        if self.wall_slide:
            wx=sx+7 if self.on_wall==1 else sx
            pyxel.pset(wx,sy+2,suit); pyxel.pset(wx,sy+3,7)


# ── Boss (3 Phasen) ───────────────────────────────────────
class Boss:
    MAX_HP = 18
    SIZE   = 14

    def __init__(self, x, y):
        self.x, self.y      = float(x), float(y)
        self.hp             = self.MAX_HP
        self.vx             = 0.0
        self.vy             = 0.0
        self.dir            = 1
        self.anim           = 0

        # ── Shot timers (fewer shots) ──
        self.shot_cd        = 120
        self.spiral_ang     = 0

        # ── Ram attack (replaces charge) ──
        # Ram: rush at player → knock them back (no damage)
        self.ram_cd         = 200   # frames between rams
        self.ram_timer      = 0     # frames of active ram
        self.ram_vx         = 0.0
        self.ram_warn       = 0     # warning flash frames

        # ── Shield (phase 3) ──
        self.shield_cd      = 140
        self.shield_timer   = 0
        self.shielded       = False

        # ── Phase-transition flash ──
        self.trans_flash    = 0

        # ── Spawn cd ──
        self.spawn_cd       = 0

    @property
    def phase(self):
        if self.hp > 12: return 1
        if self.hp >  6: return 2
        return 3

    def take_hit(self):
        if self.shielded: return False
        self.hp -= 1
        if self.hp == 12 or self.hp == 6:
            self.trans_flash = 40
        return True

    def update(self, platforms, shots, player, spawn_enemy_fn=None):
        self.anim = (self.anim + 1) % 60
        ph  = self.phase
        base_spd = {1: 0.7, 2: 1.2, 3: 1.9}[ph]

        # ── SHIELD (phase 3) ──────────────────────────────
        if ph == 3:
            if self.shield_timer > 0:
                self.shielded = True
                self.shield_timer -= 1
                if self.shield_timer == 0:
                    self.shielded = False
                    self.shield_cd = 140
                if self.anim % 10 == 0:
                    for i in range(4):
                        ang = math.radians(self.spiral_ang + i*90)
                        shots.append({"x": self.x+7, "y": self.y+7,
                            "vx": math.cos(ang)*1.6, "vy": math.sin(ang)*1.6,
                            "dmg": 1, "col": 2})
                    self.spiral_ang += 15
            else:
                self.shielded = False
                self.shield_cd -= 1
                if self.shield_cd <= 0:
                    self.shield_timer = 30

        # ── RAM ATTACK ────────────────────────────────────
        # Boss charges at player; on hit, pushes player away (no damage)
        if self.ram_warn > 0:
            self.ram_warn -= 1
            if self.ram_warn == 0:
                self.ram_vx    = 7.0 * (1 if player.x > self.x else -1)
                self.ram_timer = 35
        elif self.ram_timer > 0:
            self.vx = self.ram_vx
            self.ram_timer -= 1
            # Check if boss hits player during ram
            if abs(self.x - player.x) < 16 and abs(self.y - player.y) < 14:
                # Push player away — no hp damage
                push_dir = 1 if player.x >= self.x else -1
                player.vx = push_dir * 5.5
                player.vy = -3.5
                player.invincible = max(player.invincible, 40)
                self.ram_timer = 0   # stop ram on contact
                self.vx = base_spd * self.dir
                self.ram_cd = {1: 220, 2: 160, 3: 110}[ph]
            if self.ram_timer == 0 and self.ram_cd > 0:
                self.vx = base_spd * self.dir
                self.ram_cd = {1: 220, 2: 160, 3: 110}[ph]
        elif self.ram_cd <= 0:
            self.ram_warn = 45
            self.ram_cd   = 1
        else:
            self.ram_cd -= 1
            self.vx = base_spd * self.dir

        # ── MOVEMENT ──────────────────────────────────────
        self.vy = min(self.vy + GRAVITY * 0.5, MAX_FALL)
        self.x += self.vx; self.y += self.vy

        if self.y + self.SIZE >= H:
            self.y = H - self.SIZE; self.vy = 0
        for px2, py2, pw, ph2 in platforms:
            if self.x < px2+pw and self.x+self.SIZE > px2:
                if self.vy >= 0 and self.y+self.SIZE >= py2 and self.y+self.SIZE <= py2+ph2+self.vy+2:
                    self.y = py2 - self.SIZE; self.vy = 0

        # Edge bounce (only when not ramming)
        if self.ram_timer == 0:
            if self.x <= 5 or self.x + self.SIZE >= ARENA_W - 5:
                self.dir *= -1
                self.vx = base_spd * self.dir

        if self.trans_flash > 0:
            self.trans_flash -= 1

        # ── SHOOTING (fewer, simpler) ─────────────────────
        dx = player.x - self.x; dy = player.y - self.y
        dist = max(1, math.sqrt(dx*dx+dy*dy))
        ba   = math.atan2(dy, dx)

        def shoot(ang, spd, dmg, col):
            shots.append({"x": self.x+7, "y": self.y+7,
                "vx": math.cos(ang)*spd, "vy": math.sin(ang)*spd,
                "dmg": dmg, "col": col})

        self.shot_cd -= 1
        if self.shot_cd <= 0 and not self.shielded and self.ram_timer == 0:
            self.shot_cd = {1: 200, 2: 150, 3: 100}[ph]
            pyxel.play(3,8)
            if ph == 1:
                # Single aimed shot
                shoot(ba, 1.8, 1, 8)
            elif ph == 2:
                # Triple spread
                for s in [-18, 0, 18]:
                    shoot(ba + math.radians(s), 2.0, 1, 8)
            elif ph == 3:
                # 4-way spiral + aimed
                for i in range(4):
                    shoot(math.radians(self.spiral_ang + i*90), 2.4, 1, 9)
                self.spiral_ang += 45
                shoot(ba, 2.8, 1, 8)
        if spawn_enemy_fn and ph >= 2:
            self.spawn_cd -= 1
            if self.spawn_cd <= 0:
                self.spawn_cd = {2: 240, 3: 160}[ph]
                spawn_enemy_fn(self.x + 20*self.dir, self.y - 2)

    def draw(self, cam_x):
        sx = int(self.x - cam_x); sy = int(self.y)
        ph = self.phase; t = self.anim
        body  = {1: 2,  2: 8,  3: 9 }[ph]
        upper = {1: 2,  2: 2,  3: 8 }[ph]
        glow  = {1: 14, 2: 8,  3: 9 }[ph]

        # Phase-transition flash
        if self.trans_flash > 0 and self.trans_flash % 4 < 2:
            pyxel.rect(sx-2, sy-2, self.SIZE+4, self.SIZE+4, 7)
            return

        # Aura (phase 2+)
        if ph >= 2:
            r = 9 + (t % 20) // 5
            for ang in range(0, 360, 18):
                rad = math.radians(ang + t*4)
                ax = int(sx+7+r*math.cos(rad)); ay = int(sy+7+r*math.sin(rad)*0.65)
                if 0<=ax<W and 0<=ay<H: pyxel.pset(ax, ay, glow)

        # Ram warning flash
        if self.ram_warn > 0 and t % 6 < 3:
            pyxel.rectb(sx-2, sy-2, self.SIZE+4, self.SIZE+4, 10)

        # Shield shimmer
        if self.shielded and t % 4 < 2:
            pyxel.rectb(sx-1, sy-1, self.SIZE+2, self.SIZE+2, 14)

        # Body
        pyxel.rect(sx,   sy+5, self.SIZE, 8, body)
        pyxel.rect(sx+3, sy+2, 8, 5, upper)
        pyxel.rect(sx+5, sy,   4, 3, upper)

        # Cockpit window
        win = {1:12, 2:11, 3:10}[ph]
        pyxel.rect(sx+5, sy+3, 4, 3, win)
        pyxel.pset(sx+6, sy+4, 7); pyxel.pset(sx+8, sy+4, 7)

        # Thrusters
        pyxel.rect(sx+1,  sy+12, 3, 2, 5)
        pyxel.rect(sx+10, sy+12, 3, 2, 5)
        fire = 10 if t%4<2 else 9
        pyxel.pset(sx+2, sy+14, fire); pyxel.pset(sx+11, sy+14, fire)

        # Cannons (more per phase)
        pyxel.rect(sx,    sy+7, 2, 2, 13)
        pyxel.rect(sx+12, sy+7, 2, 2, 13)
        if ph >= 2:
            pyxel.rect(sx+3, sy+13, 2, 3, 13)
            pyxel.rect(sx+9, sy+13, 2, 3, 13)
        if ph == 3:
            pyxel.rect(sx-2, sy+5, 2, 2, 13)
            pyxel.rect(sx+14,sy+5, 2, 2, 13)
            pyxel.pset(sx+4, sy+6, 0); pyxel.pset(sx+9, sy+8, 0)

        # HP bar
        bw=54; bx=sx+7-bw//2; by=sy-10
        pyxel.rect(bx, by, bw, 3, 5)
        pyxel.rect(bx, by, int(bw*max(self.hp,0)/self.MAX_HP), 3, {1:8,2:10,3:9}[ph])
        pyxel.rectb(bx, by, bw, 3, 7)
        for frac in [12/18, 6/18]:
            mx2 = bx+int(bw*frac); pyxel.line(mx2,by,mx2,by+2,7)
        label = f"BOSS PH.{ph}" + (" [SHIELD]" if self.shielded else "")
        pyxel.text(bx, by-7, label, 14 if self.shielded else 7)


# ── Gegner ────────────────────────────────────────────────
class Enemy:
    def __init__(self, x, y, kind="drone"):
        self.x,self.y=float(x),float(y); self.kind=kind
        self.hp=3   # all enemies take 3 hits
        self.vy=0.0
        self.vx=0.5 if kind=="drone" else 0.0   # shooters are stationary turrets
        self.dir=1; self.cd=0; self.anim=0; self.dead=False; self.size=8
    def update(self, platforms, shots, player, has_floor=False):
        self.anim=(self.anim+1)%30; self.vy=min(self.vy+GRAVITY,MAX_FALL)
        # Shooters don't walk at all — they are stationary turrets
        if self.kind=="drone":
            self.x+=self.vx*self.dir
        self.y+=self.vy; grounded=False
        if has_floor and self.y+self.size>=H: self.y=H-self.size; self.vy=0; grounded=True
        prev_bot = self.y + self.size - self.vy
        for px2,py2,pw,ph in platforms:
            if self.x<px2+pw and self.x+self.size>px2:
                if self.vy>=0 and self.y+self.size>=py2 and prev_bot<=py2+4:
                    self.y=py2-self.size; self.vy=0; grounded=True
        if grounded and self.kind=="drone":
            edge=True
            for px2,py2,pw,ph in platforms:
                nx=self.x+self.dir*(self.size+1)
                if px2<=nx<=px2+pw and self.y+self.size+1>=py2: edge=False; break
            if edge: self.dir*=-1
        if self.kind=="shooter":
            self.cd-=1
            # Only shoot when visible (within one screen width of player)
            # Visible if within approximate camera frustum: cam ≈ player.x - W//3
            cam_left  = player.x - W//3 - 24
            cam_right = player.x + 2*W//3 + 24
            if self.cd<=0 and cam_left <= self.x <= cam_right:
                self.cd=120; dx=player.x-self.x; dy=player.y-self.y
                dist=max(1,math.sqrt(dx*dx+dy*dy))
                shots.append({"x":self.x+4,"y":self.y+4,
                    "vx":dx/dist*1.8,"vy":dy/dist*1.8,"dmg":1,"col":8})
    def draw(self, cam_x):
        sx=int(self.x-cam_x); sy=int(self.y); t=self.anim
        if self.kind=="drone":
            pyxel.circ(sx+4,sy+4,3,11); pyxel.pset(sx+4,sy+4,3)
            pyxel.pset(sx+3,sy+3,7); pyxel.pset(sx+5,sy+3,7)
            off=1 if t<15 else -1
            pyxel.line(sx+4,sy,sx+4+off*3,sy-2,7); pyxel.line(sx+4,sy,sx+4-off*3,sy-2,7)
        else:
            pyxel.rect(sx+1,sy+2,6,4,8); pyxel.rect(sx+3,sy,2,2,2)
            pyxel.pset(sx+3,sy+1,7); pyxel.rect(sx+6,sy+3,3,2,13)
            if t%4<2: pyxel.pset(sx,sy+3,10); pyxel.pset(sx,sy+4,9)


# ══════════════════════════════════════════════════════════
#  SECTION-BUILDER — Abschnitts-Generatoren
# ══════════════════════════════════════════════════════════

def _place_on_plat(plat, kind, enemies):
    px2, py2, pw, _ = plat
    if pw < 16: return
    e = Enemy(px2 + pw//2 - 4, py2 - 10, kind)
    e.vy = 0.0
    enemies.append(e)


def sec_safe(x, length, lvl, plats, moving_plats, enemies, coins, spikes,
             health_packs, entry_y=None, exclusion_zones=None):
    """Floating platforms connected smoothly.
    Rules (all verified with physics vy=-4.5, g=0.35, vx_max=2.0):
      • Max vertical jump height  = 4.5²/(2*0.35) ≈ 29 px → max dy ± 12 px
      • Max horizontal jump range  = vx * airtime ≈ 2.0*26 ≈ 52 px
        → gap between platforms ≤ 42 px (leave margin)
    Every LARGE platform (≥50 px wide) is replaced by a MOVING platform
    that travels within the same bounding box.  Smaller platforms stay static.
    """
    if entry_y is None:
        entry_y = H - 55
    if exclusion_zones is None:
        exclusion_zones = []
    cx   = x
    # ── First platform exactly at entry_y (no offset) so sections connect ──
    y    = clamp(entry_y, 58, H-40)
    ey   = y
    first = True
    last_right = x   # fallback if length==0

    while cx < x + length:
        w  = random.randint(34, 50)
        if not first:
            dy = random.randint(-6, 6)
            y  = clamp(y + dy, 58, H-40)
        first = False
        ey = y

        # Check: is this x position inside an exclusion zone?
        in_excl = any(ez_l - 4 <= cx + w and ez_r + 4 >= cx
                      for ez_l, ez_r in exclusion_zones)

        # Check: is the previous placed platform a moving one at similar height?
        last_is_moving = (len(moving_plats) > 0
                          and "_zone_x" in moving_plats[-1]
                          and abs(moving_plats[-1]["y"] - y) <= 14)

        use_moving = (not in_excl and not last_is_moving
                      and random.random() < 0.25)

        if use_moving:
            # Double-width zone → moving platform travels within it
            big_w = w * 2 + random.randint(4, 14)
            mw    = max(28, w - 4)       # moving part ≈ normal width
            mspd  = random.choice([0.7, 1.0, 1.2]) * random.choice([-1, 1])

            # Clamp zone against exclusion zones
            zone_l = float(cx)
            zone_r = float(cx + big_w)
            for ez_l, ez_r in exclusion_zones:
                if ez_l < zone_r and ez_r > zone_l:
                    if ez_l >= (zone_l + zone_r) / 2:
                        zone_r = min(zone_r, ez_l - 2)
                    else:
                        zone_l = max(zone_l, ez_r + 2)
            # Clamp against existing moving platform zones
            for oth in moving_plats:
                if "_zone_x" not in oth or abs(oth["y"] - y) > 14: continue
                oz_l = oth["_zone_x"]; oz_r = oz_l + oth["_zone_w"]
                if oz_l < zone_r and oz_r > zone_l:
                    if oz_l >= (zone_l + zone_r) / 2:
                        zone_r = min(zone_r, oz_l - 2)
                    else:
                        zone_l = max(zone_l, oz_r + 2)

            if zone_r - zone_l >= mw + 8:
                # Start at left edge of zone so full range is visible immediately
                mx0 = zone_l
                moving_plats.append({
                    "x": float(mx0), "y": y, "w": mw, "vx": abs(mspd),  # start moving right
                    "min": float(zone_l),
                    "max": float(zone_r - mw),   # rightmost x where platform fits
                    "_zone_x": zone_l, "_zone_w": zone_r - zone_l
                })
                mid_x      = int((zone_l + zone_r) / 2)
                last_right = cx + big_w
                cx += big_w + random.randint(14, 32)
                for j in range(random.randint(0, 3)):
                    coins.append({"x": mid_x + j*12 - 12, "y": y - 9,
                                  "collected": False})
                continue  # skip static logic

        # Static platform
        plats.append((cx, y, w, 6))
        mid_x      = cx + w // 2
        last_right = cx + w
        if random.random() < 0.40:
            kind = "shooter" if random.random() < 0.25 else "drone"
            _place_on_plat((cx, y, w, 6), kind, enemies)
        if random.random() < 0.13:
            health_packs.append({"x": cx + w//2, "y": y - 8, "taken": False})
        if random.random() < 0.16 and w > 40:
            spikes.append((cx + w - 7, y - 4))
        cx += w + random.randint(16, 34)
        for j in range(random.randint(0, 3)):
            coins.append({"x": mid_x + j*12 - 12, "y": y - 9,
                          "collected": False})
    return last_right, ey


def sec_grapple(x, entry_y, lvl, plats, hints, coins):
    """
    GRAPPLE GATE — physically impossible to jump across.
    Gap from left-pad-end to landing ≥ 87 px (max jump 52 px).
    Landing is also 32 px ABOVE left pad (max jump height 29 px).
    → Double-impossible without grapple.

    Hook is 52 px above left pad (fits grapple reach).
    Swing right → release Z → land on the LANDING platform.
    Landing → Right-pad gap = 18 px (normal jump).
    """
    y = clamp(entry_y, 72, H-46)
    LW=60; HW=40; LANDW=50; RW=60

    plats.append((x, y, LW, 6))                      # left pad

    # Place hook so the grapple trajectory (angle 0.39 right, 0.92 up)
    # fired from left-pad center lands in the MIDDLE of the hook platform.
    # pad_cx = x + LW//2, fire_y = y - 4 (player mid-body when standing on pad)
    # Trajectory: x_offset = 0.424 * vertical_distance
    hx = x + 14                                       # hook: above left pad
    hy = clamp(y - 52, 18, y - 40)
    plats.append((hx, hy, HW, 6))
    coins.append({"x": hx+HW//2, "y": hy-8, "collected": False})

    # Landing: right of hook, 20 px lower than hook
    lx = hx + HW + 35
    ly = clamp(hy + 20, hy + 14, y - 10)
    plats.append((lx, ly, LANDW, 6))
    coins.append({"x": lx+LANDW//2, "y": ly-8, "collected": False})

    rx  = lx + LANDW + 18
    ry  = ly

    plats.append((rx, ry, RW, 6))                     # right pad (Auffangplattform)

    # Step-up platform: a bit higher than the landing pad, bridging upward
    step_x = rx + RW + 14
    step_y = clamp(ry - 12, 30, ry - 8)   # 8-12px above landing pad
    plats.append((step_x, step_y, 44, 6))

    # Second bridge at step height so sec_safe connects smoothly
    bridge_x = step_x + 58
    plats.append((bridge_x, step_y, 44, 6))

    hints.append({"x": x+2, "y": y-36,
        "lines": [("GRAPPLE UP", 11), ("SWING  >>>", 3)], "icon": "G"})
    return bridge_x + 44, step_y


def sec_dash(x, entry_y, lvl, plats, spikes, hints):
    """
    DASH GATE — spike corridor with a FULL ceiling from HUD (y=16) down to
    player-top level (y-8).  The ceiling blocks any jump attempt (ceiling
    collision in Player.update).  Dash = 18-frame invincibility → sprint
    through half as many spikes as before.
    """
    y  = clamp(entry_y, 72, H-46)
    n  = 4   # 4 spikes
    cw = n * 7

    plats.append((x,    y, 52, 6))            # left pad
    plats.append((x+52, y, cw, 6))            # corridor floor

    # Full ceiling: from HUD line (y=16) to exactly player-top level (y-8).
    # Height = (y-8) - 16 = y-24.  Bottom of ceiling = y-8 = player top
    # when standing → any upward movement immediately triggers ceiling collision.
    ceil_top = 16
    ceil_h   = max(4, (y - 8) - ceil_top)
    plats.append((x+52, ceil_top, cw, ceil_h))  # solid wall from HUD to corridor

    for i in range(n):
        spikes.append((x+52 + i*7, y - 5))   # spikes on corridor floor
    end = x + 52 + cw
    plats.append((end, y, 52, 6))             # right pad

    hints.append({"x": x+2, "y": y-36,
        "lines": [("DASH THRU", 9), ("S = DASH", 8)], "icon": "D"})
    return end + 52, y


def sec_wall(x, entry_y, lvl, plats, hints):
    """
    WALL-JUMP GATE:

        Ausgang (open, no platform)
           ↑ gap ↑
      [=cap_L=]   [=cap_R=]
          |   inner 28px |
      left wall       right wall
          |               |
     [hole 10px]          |
      ╔══════════════════╗
      ║  shaft floor     ║
      entry pad >>>

    - Entry:  hole at the BOTTOM of the LEFT wall (10 px tall).
              Player walks along entry pad, steps through hole, enters shaft.
    - Walls:  solid above the hole; left wall = left of hole + above hole.
    - Caps:   left cap extends LEFT, right cap extends RIGHT at top of walls.
    - Exit:   top is fully OPEN (no platform). Player wall-jumps up and out.
    """
    col_w   = 8
    inner_w = 28
    cap_w   = 20
    cap_h   = 5
    hole_h  = 10   # entry hole height (player SIZE=8, 2px margin)

    max_shaft = max(55, entry_y - 24)
    shaft_h   = min(84, max_shaft)
    ey   = clamp(entry_y, shaft_h + 24, H - 42)
    top_y = clamp(ey - shaft_h, 22, ey - shaft_h)

    shaft_x = x

    # ── Entry pad (left of shaft) ──────────────────────────
    plats.append((shaft_x - 60, ey, 60, 6))

    # ── Shaft floor ────────────────────────────────────────
    plats.append((shaft_x, ey, col_w * 2 + inner_w, 6))

    # ── Left wall with hole at the bottom ──────────────────
    # Hole sits at the bottom of the wall (ey - hole_h to ey).
    # Top segment: top_y → (ey - hole_h)
    top_seg_h = (ey - hole_h) - top_y
    if top_seg_h > 0:
        plats.append((shaft_x, top_y, col_w, top_seg_h))
    # Bottom segment: (ey) → void (below shaft floor, extends down)
    # We don't need a bottom segment below the hole since shaft floor blocks passage.
    # But we need wall below hole_top for any y below ey (the walls go into void).
    # Left wall below hole: not needed visually but prevents side-exit below.
    # The floor platform itself stops the player from going down.

    # ── Right wall: full height ─────────────────────────────
    rwall_x = shaft_x + col_w + inner_w
    plats.append((rwall_x, top_y, col_w, ey - top_y))

    # ── Caps ────────────────────────────────────────────────
    # Left cap extends LEFT from top of left wall
    plats.append((shaft_x - cap_w, top_y - cap_h, cap_w + col_w, cap_h))

    # Right cap extends RIGHT from top of right wall
    plats.append((rwall_x, top_y - cap_h, col_w + cap_w, cap_h))

    # ── NO exit platform at top — path is open upward ───────

    hints.append({"x": shaft_x - 56, "y": ey - 28,
        "lines": [("< HOLE", 10), ("WALL JUMP ^", 3)], "icon": "W"})

    # Return: right edge of right cap + buffer, height just above caps
    return rwall_x + col_w + cap_w + 10, top_y - cap_h - 8

def sec_combined(x, entry_y, lvl, plats, spikes, hints, coins):
    """COMBO GATE (Level 3): Grapple cross → immediately dash through spikes."""
    y = clamp(entry_y, 72, H-46)
    LW=52; HW=36; LANDW=46

    plats.append((x, y, LW, 6))

    hx = x + 12
    hy = clamp(y - 50, 18, y - 38)
    plats.append((hx, hy, HW, 6))
    coins.append({"x": hx+HW//2, "y": hy-8, "collected": False})

    lx = hx + HW + 35
    ly = clamp(hy + 18, hy + 12, y - 10)
    plats.append((lx, ly, LANDW, 6))

    n  = 4   # fixed 4 spikes
    cw = n * 7
    plats.append((lx+LANDW,    ly,    cw, 6))   # corridor floor
    # Full ceiling from HUD (y=16) to just above player head (ly-8)
    ceil_top = 16
    ceil_h   = max(4, (ly - 8) - ceil_top)
    plats.append((lx+LANDW, ceil_top, cw, ceil_h))  # ceiling all the way up
    for i in range(n):
        spikes.append((lx + LANDW + i*7, ly - 5))

    ex  = lx + LANDW + cw
    ey2 = clamp(ly + random.randint(-8, 8), 55, H-38)
    plats.append((ex, ey2, 54, 6))

    hints.append({"x": x+2, "y": y-38,
        "lines": [("GRAPPLE", 11), ("then DASH!", 9)], "icon": "G"})
    return ex + 54, ey2


def fix_moving_plat_bounds(moving_plats, static_plats):
    """Only removes the _zone_x helper keys. Min/max are already
    set correctly at generation time in sec_safe."""
    for mp in moving_plats:
        if "_zone_x" in mp:
            del mp["_zone_x"], mp["_zone_w"]

# ── Level-Generator ───────────────────────────────────────
def generate_level(lvl):
    plats=[]; moving_plats=[]; spikes=[]; enemies=[]
    enemy_shots=[]; coins=[]; health_packs=[]; hints=[]

    START_Y = H - 50
    x = 10
    plats.append((x, START_Y, 80, 6))   # Start-Pad
    x  += 100
    cy  = START_Y
    ez  = []   # exclusion zones: list of (x_start, x_end) that moving plats must avoid

    GAP = 15   # px gap between gate end and next sec_safe

    def safe(sx, length, entry_y):
        """Call sec_safe with current exclusion zones."""
        return sec_safe(sx, length, lvl, plats, moving_plats, enemies,
                        coins, spikes, health_packs, entry_y, ez)

    def gate_grapple(sx, ey):
        x0 = sx
        rx, ry = sec_grapple(sx, ey, lvl, plats, hints, coins)
        ez.append((x0 - 10, rx + 10))   # reserve this x-range
        return rx, ry

    def gate_dash(sx, ey):
        x0 = sx
        rx, ry = sec_dash(sx, ey, lvl, plats, spikes, hints)
        ez.append((x0 - 10, rx + 10))
        return rx, ry

    def gate_wall(sx, ey):
        x0 = sx
        rx, ry = sec_wall(sx, ey, lvl, plats, hints)
        ez.append((x0 - 10, rx + 10))
        return rx, ry

    def gate_combined(sx, ey):
        x0 = sx
        rx, ry = sec_combined(sx, ey, lvl, plats, spikes, hints, coins)
        ez.append((x0 - 10, rx + 10))
        return rx, ry

    if lvl == 1:
        x, cy = safe(x, 240, cy)
        x, cy = gate_grapple(x+20, cy)
        x, cy = safe(x+GAP, 260, cy)
        x, cy = gate_wall(x+20, cy)
        x, cy = safe(x+GAP, 240, cy)

    elif lvl == 2:
        x, cy = safe(x, 200, cy)
        x, cy = gate_grapple(x+20, cy)
        x, cy = safe(x+GAP, 180, cy)
        x, cy = gate_dash(x+20, cy)
        x, cy = safe(x+GAP, 180, cy)
        x, cy = gate_wall(x+80, cy)
        x, cy = safe(x+GAP, 200, cy)
        x, cy = gate_grapple(x+20, cy)
        x, cy = safe(x+GAP, 200, cy)

    elif lvl == 3:
        x, cy = safe(x, 160, cy)
        x, cy = gate_wall(x+80, cy)
        x, cy = safe(x+GAP, 140, cy)
        x, cy = gate_grapple(x+20, cy)
        x, cy = gate_dash(x+GAP, cy)
        x, cy = safe(x+GAP, 140, cy)
        x, cy = gate_combined(x+20, cy)
        x, cy = gate_wall(x+80, cy)
        x, cy = safe(x+GAP, 160, cy)
        x, cy = gate_grapple(x+20, cy)
        x, cy = safe(x+GAP, 160, cy)

    # Fix moving platform bounds so they never overlap static platforms
    fix_moving_plat_bounds(moving_plats, plats)

    plats.append((x, cy, 60, 6))          # final approach platform
    plats.append((x + 74, cy, 44, 6))      # bridge to portal zone
    goal_x = x + 74 + 44 + 10             # portal right after bridge
    goal_y = cy                            # height of final platform

    return (plats, moving_plats, spikes, enemies, enemy_shots,
            coins, health_packs, hints, goal_x, goal_y)


# ── Boss-Arena-Generator ──────────────────────────────────
def generate_boss_arena(lvl):
    """Boss dimension: wide moon floor, no platforms, boss spawns on floor."""
    plats=[]; moving_plats=[]; enemies=[]; shots=[]
    # Wide floor only — no floating islands, no platforms
    floor_h = 18
    plats.append((0, H - floor_h, ARENA_W, floor_h))
    # Boss spawns standing on the floor
    boss_x = float(ARENA_W // 2 - 7)
    boss_y = float(H - floor_h - Boss.SIZE)
    boss = Boss(boss_x, boss_y)
    return plats, moving_plats, enemies, shots, boss


# ══════════════════════════════════════════════════════════
#   GAME
# ══════════════════════════════════════════════════════════
class Game:
    def __init__(self):
        pyxel.init(W,H,title="SPACE PLATFORMER v4",fps=60)
        self._setup_sounds()
        self.stars=StarField(); self.void_field=VoidField()
        self.score=0; self.state="title"; self.active_scene="play"
        self._bgm = None
        self.reset(1)
        pyxel.run(self.update,self.draw)

    def _setup_sounds(self):
        # ── SFX — Einzelnoten, einfachstes Format ──────────
        # 0: Sprung
        pyxel.sound(0).set("g3", "s", "5", "n", 8)
        # 1: Laser
        pyxel.sound(1).set("c4", "p", "6", "n", 4)
        # 2: Münze
        pyxel.sound(2).set("b4", "s", "6", "n", 5)
        # 3: Gegner getroffen
        pyxel.sound(3).set("b2", "n", "6", "n", 6)
        # 4: Gegner stirbt
        pyxel.sound(4).set("c2", "n", "7", "f", 10)
        # 5: Spieler getroffen
        pyxel.sound(5).set("f2", "n", "7", "f", 12)
        # 6: Dash
        pyxel.sound(6).set("c4", "s", "4", "n", 3)
        # 7: Boss getroffen
        pyxel.sound(7).set("a1", "n", "7", "n", 10)
        # 8: Boss schiesst
        pyxel.sound(8).set("d4", "p", "5", "n", 5)
        # 9: Portal
        pyxel.sound(9).set("c4", "s", "7", "f", 16)

        # ── BGM Level — einfache Loop-Melodie ─────────────
        pyxel.sound(16).set("c3", "p", "4", "n", 30)
        pyxel.sound(17).set("g2", "p", "3", "n", 30)
        pyxel.sound(18).set("e3", "p", "3", "n", 30)

        # ── BGM Boss — dunkler Ton ─────────────────────────
        pyxel.sound(20).set("a1", "p", "5", "n", 25)
        pyxel.sound(21).set("a0", "n", "4", "n", 25)
        pyxel.sound(22).set("c2", "p", "3", "n", 25)

    def _play_bgm(self, kind):
        if self._bgm == kind:
            return
        self._bgm = kind
        pyxel.stop(0); pyxel.stop(1); pyxel.stop(2)
        if kind == "level":
            pyxel.play(0, 16, loop=True)
            pyxel.play(1, 17, loop=True)
            pyxel.play(2, 18, loop=True)
        elif kind == "boss":
            pyxel.play(0, 20, loop=True)
            pyxel.play(1, 21, loop=True)
            pyxel.play(2, 22, loop=True)

    def reset(self, lvl=1):
        self.level=lvl
        (self.platforms,self.moving_plats,self.spikes,self.enemies,
         self.enemy_shots,self.coins,self.health_packs,
         self.hints,self.goal_x,self.goal_y)=generate_level(lvl)
        self.player=Player(20,H-70)
        self.player_shots=[]; self.particles=[]
        self.cam_x=0.0; self.shake=0; self.shake_x=0; self.transition=0
        self.arena_platforms=[]; self.arena_moving_plats=[]
        self.arena_enemies=[]; self.arena_shots=[]; self.boss=None
        self.arena_cam_x=0.0; self.portal_timer=0; self.arena_exit_timer=0
        self.arena_intro_timer=0; self.active_scene="play"

    def enter_boss_arena(self):
        (self.arena_platforms,self.arena_moving_plats,
         self.arena_enemies,self.arena_shots,self.boss)=generate_boss_arena(self.level)
        p=self.player
        floor_top = H - 18   # floor_h=18
        p.x=40.0; p.y=float(floor_top - 8); p.vx=p.vy=0.0  # land directly on floor
        p.hp=p.max_hp; p.invincible=0           # full heal on dimension entry
        p.on_ground=False; p.grapple.release(); self.player_shots=[]
        self.arena_cam_x=max(0.0, 40.0 - W//3); self.void_field=VoidField()
        self.arena_intro_timer=120; self.active_scene="arena"; self.state="boss_arena"

    def spawn_fx(self,x,y,cols,n=12):
        for _ in range(n):
            self.particles.append(Particle(x,y,
                random.uniform(-2.5,2.5),random.uniform(-3.2,0.5),
                random.choice(cols),random.randint(12,26)))

    def _void_death(self):
        p=self.player
        p.damage(6); self.spawn_fx(p.x,H-4,[2,8,14,1],30)

    # ── Update ────────────────────────────────────────────
    def update(self):
        p=self.player
        if self.state=="title":
            if self._bgm is not None: self._bgm=None; pyxel.stop(0);pyxel.stop(1);pyxel.stop(2)
            if pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_SPACE):
                self.score=0; self.reset(1); self.state="play"
            return
        if self.state in ("win","gameover"):
            if pyxel.btnp(pyxel.KEY_R): self.score=0; self.reset(1); self.state="play"
            return
        if self.state=="portal_enter":
            self.portal_timer-=1
            if self.portal_timer<=0: self.enter_boss_arena()
            return
        if self.state=="arena_exit":
            self.arena_exit_timer-=1
            if self.arena_exit_timer<=0:
                if self.level>=3: self.state="win"
                else:
                    sc=self.score; self.reset(self.level+1); self.score=sc; self.state="play"
            return
        if self.state=="play": self._update_play()
        elif self.state=="boss_arena": self._update_arena()

    def _update_play(self):
        p=self.player
        if pyxel.btnp(pyxel.KEY_X):
            self.player_shots.append({"x":p.x+(8 if p.facing==1 else 0),"y":p.y+4,"vx":5.5*p.facing})
            pyxel.play(3,1)
        # Kein Boden in normalen Leveln!
        p.update(self.platforms,self.moving_plats,self.spikes,has_floor=False)
        # Void-Tod
        if p.y+p.SIZE>H: self._void_death()
        if p.dead: self.state="gameover"; return
        target_cam=p.x-W//3; self.cam_x+=(target_cam-self.cam_x)*0.11; self.cam_x=max(0,self.cam_x)
        if self.shake>0: self.shake-=1; self.shake_x=random.randint(-2,2)
        else: self.shake_x=0
        for mp in self.moving_plats:
            mp["x"]+=mp["vx"]
            if mp["x"]<mp["min"] or mp["x"]>mp["max"]: mp["vx"]*=-1
        for shot in self.player_shots[:]:
            shot["x"]+=shot["vx"]
            if shot["x"]<self.cam_x or shot["x"]>self.cam_x+W: self.player_shots.remove(shot); continue
            # Block on solid platforms
            shot_blocked = False
            for px2,py2,pw,ph in self.platforms:
                if px2 <= shot["x"] <= px2+pw and py2 <= shot["y"] <= py2+ph:
                    self.player_shots.remove(shot); shot_blocked = True; break
            if shot_blocked: continue
            hit=False
            for e in self.enemies[:]:
                if abs(shot["x"]-e.x-4)<9 and abs(shot["y"]-e.y-4)<9:
                    e.hp-=1; self.spawn_fx(e.x+4,e.y+4,[10,9,7],5); pyxel.play(3,3)
                    if e.hp<=0: self.spawn_fx(e.x+4,e.y+4,[8,9,10,7],14); self.enemies.remove(e); self.score+=100; pyxel.play(3,4)
                    hit=True; break
            if hit and shot in self.player_shots: self.player_shots.remove(shot)
        for shot in self.enemy_shots[:]:
            shot["x"]+=shot["vx"]; shot["y"]+=shot["vy"]
            if shot["x"]<0 or shot["x"]>self.goal_x+200 or shot["y"]<0 or shot["y"]>H: self.enemy_shots.remove(shot); continue
            # Block on solid platforms
            eshot_blocked = False
            for px2,py2,pw,ph in self.platforms:
                if px2 <= shot["x"] <= px2+pw and py2 <= shot["y"] <= py2+ph:
                    self.enemy_shots.remove(shot); eshot_blocked = True; break
            if eshot_blocked: continue
            if abs(shot["x"]-p.x-4)<7 and abs(shot["y"]-p.y-4)<7:
                p.damage(shot.get("dmg",1)); self.spawn_fx(p.x+4,p.y+4,[8,9],5); self.enemy_shots.remove(shot)
        for e in self.enemies[:]:
            e.update(self.platforms,self.enemy_shots,p,has_floor=False)
            if e.y>H+20: self.enemies.remove(e); continue
            if abs(e.x-p.x)<10 and abs(e.y-p.y)<10: p.damage(1)
        for c in self.coins:
            if not c["collected"] and abs(c["x"]-p.x-4)<9 and abs(c["y"]-p.y-4)<9:
                c["collected"]=True; self.score+=50; self.spawn_fx(c["x"],c["y"],[9,10,7],5); pyxel.play(3,2)
        for hp in self.health_packs:
            if not hp["taken"] and abs(hp["x"]-p.x-4)<9 and abs(hp["y"]-p.y-4)<9:
                hp["taken"]=True; p.heal(2); self.spawn_fx(hp["x"],hp["y"],[8,7],8)
        self.particles=[pp for pp in self.particles if pp.life>0]
        for pp in self.particles: pp.update()
        if p.x>=self.goal_x:
            if self.level < 3:
                # Kein Boss in Level 1 und 2 — direkt weiter
                sc=self.score; self.reset(self.level+1); self.score=sc; self.state="play"
            else:
                # Level 3 → Boss-Dimension
                self.state="portal_enter"; self.portal_timer=80; pyxel.play(3,9)

    def _update_arena(self):
        p=self.player
        if self.arena_intro_timer>0: self.arena_intro_timer-=1
        if pyxel.btnp(pyxel.KEY_X):
            self.player_shots.append({"x":p.x+(8 if p.facing==1 else 0),"y":p.y+4,"vx":5.5*p.facing})
            pyxel.play(3,1)
        p.update(self.arena_platforms,self.arena_moving_plats,[],has_floor=True)
        # No void death in boss arena — there is a floor
        if p.dead: self.state="gameover"; return
        target_cam=p.x-W//3
        self.arena_cam_x+=(target_cam-self.arena_cam_x)*0.12
        self.arena_cam_x=clamp(self.arena_cam_x,0,max(0,ARENA_W-W))
        if self.shake>0: self.shake-=1; self.shake_x=random.randint(-2,2)
        else: self.shake_x=0
        for mp in self.arena_moving_plats:
            mp["x"]+=mp["vx"]
            if mp["x"]<mp["min"] or mp["x"]>mp["max"]: mp["vx"]*=-1
        for shot in self.player_shots[:]:
            shot["x"]+=shot["vx"]
            if shot["x"]<self.arena_cam_x or shot["x"]>self.arena_cam_x+W: self.player_shots.remove(shot); continue
            hit=False
            for e in self.arena_enemies[:]:
                if abs(shot["x"]-e.x-4)<9 and abs(shot["y"]-e.y-4)<9:
                    e.hp-=1; self.spawn_fx(e.x+4,e.y+4,[10,9,7],5)
                    if e.hp<=0: self.spawn_fx(e.x+4,e.y+4,[8,9,10,7],14); self.arena_enemies.remove(e); self.score+=100
                    hit=True; break
            if not hit and self.boss:
                b=self.boss
                if abs(shot["x"]-b.x-7)<13 and abs(shot["y"]-b.y-7)<13:
                    if b.take_hit():
                        self.spawn_fx(b.x+7,b.y+7,[10,9,7],7)
                        self.shake=max(self.shake,4); pyxel.play(3,7)
                    else:
                        # Shield deflect spark
                        self.spawn_fx(b.x+7,b.y+7,[2,14],4)
                    if b.hp<=0:
                        self.spawn_fx(b.x+7,b.y+7,[8,9,10,7,2,14],45)
                        self.boss=None; self.score+=1500; self.shake=45
                        self.arena_exit_timer=90; self.state="arena_exit"
                    hit=True
            if hit and shot in self.player_shots: self.player_shots.remove(shot)
        for shot in self.arena_shots[:]:
            shot["x"]+=shot["vx"]; shot["y"]+=shot["vy"]
            if shot["x"]<-50 or shot["x"]>ARENA_W+100 or shot["y"]<0 or shot["y"]>H+60:
                self.arena_shots.remove(shot); continue
            if abs(shot["x"]-p.x-4)<7 and abs(shot["y"]-p.y-4)<7:
                p.damage(shot.get("dmg",1)); self.spawn_fx(p.x+4,p.y+4,[8,9],5); self.arena_shots.remove(shot)
        for e in self.arena_enemies[:]:
            e.update(self.arena_platforms,self.arena_shots,p,has_floor=False)
            if e.y>H+20: self.arena_enemies.remove(e); continue
            if abs(e.x-p.x)<10 and abs(e.y-p.y)<10: p.damage(1)
        if self.boss:
            def _spawn_minion(ex, ey):
                e2 = Enemy(ex, ey, "drone")
                e2.vy = 0.0
                self.arena_enemies.append(e2)
            self.boss.update(self.arena_platforms, self.arena_shots, p, _spawn_minion)
            if not self.boss.shielded and abs(self.boss.x-p.x)<15 and abs(self.boss.y-p.y)<15:
                p.damage(2)
        self.void_field.update()
        self.particles=[pp for pp in self.particles if pp.life>0]
        for pp in self.particles: pp.update()

    # ── Draw ──────────────────────────────────────────────
    def draw(self):
        if self.state=="title":
            pyxel.cls(1); self.stars.draw(0); self._draw_title(); return
        if self.state=="portal_enter": self._draw_portal(); return
        if self.state=="arena_exit":   self._draw_arena_exit(); return
        if self.active_scene=="arena" or self.state=="boss_arena":
            self._draw_arena(); return
        self._draw_play()

    # ─── Normal Level ────────────────────────────────────
    def _draw_play(self):
        pyxel.cls(1)
        cam=self.cam_x+self.shake_x
        self.stars.draw(cam)
        # Planet
        px2=int(200-cam*0.04)%(W+80)-40
        pyxel.circ(px2,28,18,2); pyxel.circ(px2,28,13,5); pyxel.circ(px2-5,23,5,2)
        for ang in range(0,180,10):
            rad=math.radians(ang); rx=int(px2+24*math.cos(rad)); ry=int(28+7*math.sin(rad))
            if 0<=rx<W: pyxel.pset(rx,ry,13)
        f=pyxel.frame_count
        # Void-Abgrund (Bildschirmrand unten)
        for bx2 in range(0,W,3):
            col=[1,2,5][(bx2//3+f//5)%3]
            pyxel.pset(bx2,H-1,col)
            if bx2%6==0: pyxel.pset(bx2,H-2,col)
        if (f//20)%2==0: pyxel.text(W//2-28,H-10,"~~ VOID BELOW ~~",5)
        # Plattformen
        for platx2,platy2,platw,plath in self.platforms:
            sx=int(platx2-cam)
            if -platw<sx<W:
                pyxel.rect(sx,platy2,platw,plath,5)
                pyxel.line(sx,platy2,sx+platw-1,platy2,6)
                pyxel.line(sx,platy2+1,sx+platw-1,platy2+1,13)
        # Moving platforms
        for mp in self.moving_plats:
            sx=int(mp["x"]-cam); mw=mp["w"]
            if -mw<sx<W:
                pyxel.rect(sx,mp["y"],mw,5,3); pyxel.line(sx,mp["y"],sx+mw-1,mp["y"],11)
                pyxel.pset(sx+2,mp["y"]+5,10 if f%4<2 else 9)
                pyxel.pset(sx+mw-3,mp["y"]+5,10 if f%4<2 else 9)
        # Spikes
        for spx,spy in self.spikes:
            sx=int(spx-cam)
            if -8<sx<W: pyxel.tri(sx,spy+4,sx+2,spy,sx+4,spy+4,8); pyxel.pset(sx+2,spy,9)
        # Coins
        for c in self.coins:
            if not c["collected"]:
                sx=int(c["x"]-cam)
                if -8<sx<W:
                    col=10 if (f//15)%2==0 else 9
                    pyxel.circ(sx,int(c["y"]),2,col); pyxel.pset(sx,int(c["y"]),7)
        # Health
        for hp in self.health_packs:
            if not hp["taken"]:
                sx=int(hp["x"]-cam)
                if -8<sx<W:
                    pyxel.rect(sx-2,int(hp["y"])-2,7,7,0); pyxel.rectb(sx-2,int(hp["y"])-2,7,7,8)
                    pyxel.rect(sx,int(hp["y"])-1,3,1,8); pyxel.rect(sx+1,int(hp["y"])-2,1,3,8)
        # ─ Fähigkeits-Hinweisschilder ─
        self._draw_hints(cam)
        gx=int(self.goal_x-cam)
        gy=self.goal_y   # y of final platform
        if -30<gx<W+10:
            f2=pyxel.frame_count
            if self.level == 3:
                # Dimensionsportal zum Boss — sits on top of final platform
                portal_top = gy - 26
                for i in range(18):
                    rad=math.radians(i*20+f2*5)
                    rx=int(gx+10+13*math.cos(rad)); ry=int(gy-16+8*math.sin(rad))
                    if 0<=rx<W and 0<=ry<H: pyxel.pset(rx,ry,[3,11,7,10,2,14][i%6])
                pyxel.rectb(gx+2,gy-26,16,26,14)
                for py3 in range(gy-24,gy,2):
                    off=int(math.sin(py3*0.5+f2*0.2)*2)
                    bx3=gx+10+off
                    if 0<=bx3<W and 0<=py3<H: pyxel.pset(bx3,py3,2 if (f2//8)%2==0 else 14)
                pyxel.text(gx-10,gy-34,"BOSS REALM",14 if (f2//12)%2==0 else 9)
            else:
                # Normaler Ausgang — sits on top of final platform
                for i in range(10):
                    rad=math.radians(i*36+f2*3)
                    rx=int(gx+10+8*math.cos(rad)); ry=int(gy-12+5*math.sin(rad))
                    if 0<=rx<W and 0<=ry<H: pyxel.pset(rx,ry,[11,7,3][i%3])
                pyxel.rectb(gx+2,gy-20,16,20,11)
                pyxel.text(gx-4,gy-28,"EXIT",11 if (f2//15)%2==0 else 7)
        # Gegner
        for e in self.enemies:
            if -16<int(e.x-cam)<W+16: e.draw(cam)
        # Schüsse
        for shot in self.enemy_shots:
            sx=int(shot["x"]-cam)
            if 0<sx<W: pyxel.circ(sx,int(shot["y"]),2,shot.get("col",8)); pyxel.pset(sx,int(shot["y"]),9)
        for shot in self.player_shots:
            sx=int(shot["x"]-cam)
            if 0<sx<W: pyxel.line(sx-4,int(shot["y"]),sx+4,int(shot["y"]),11); pyxel.pset(sx,int(shot["y"]),7)
        for pp in self.particles: pp.draw()
        self.player.draw(cam)
        self._draw_hud()
        # Progress-Leiste
        prog=clamp(self.player.x/max(1,self.goal_x),0,1)
        pyxel.rect(2,H-16,int(prog*(W-4)),2,11)
        pyxel.rect(2+int(prog*(W-4)),H-16,W-4-int(prog*(W-4)),2,5)
        # Overlays
        if self.state=="gameover": self._draw_gameover()
        if self.state=="win":      self._draw_win()

    def _draw_hints(self, cam):
        f=pyxel.frame_count
        for h in self.hints:
            sx=int(h["x"]-cam)
            if not -80<sx<W+20: continue
            icon=h.get("icon","?")
            # Schild-Hintergrund
            lw=max(len(l[0]) if isinstance(l,tuple) else len(l) for l in h["lines"])
            bw=lw*4+10; bh=len(h["lines"])*8+6
            by=h["y"]
            icon_col={"G":11,"D":9,"W":10,"C":14}.get(icon,7)
            # Pulsierender Rahmen
            pulse_col=icon_col if (f//15)%2==0 else 5
            pyxel.rect(sx-2,by-2,bw+4,bh+4,0)
            pyxel.rectb(sx-2,by-2,bw+4,bh+4,pulse_col)
            # Icon
            pyxel.rect(sx-1,by-1,8,8,icon_col)
            pyxel.text(sx+1,by+1,icon,0)
            # Text-Zeilen
            for i,(line_text,line_col) in enumerate(h["lines"]):
                pyxel.text(sx+10,by+i*8,line_text,line_col if isinstance(line_col,int) else icon_col)
            # Animierter Pfeil nach rechts
            ax=sx+bw+6+(3 if (f//8)%2==0 else 0)
            pyxel.text(ax,by+bh//2-2,">>",icon_col)

    # ─── Boss-Arena ──────────────────────────────────────
    def _draw_arena(self):
        cam=self.arena_cam_x+self.shake_x
        pyxel.cls(0); self.void_field.draw(cam)
        f=pyxel.frame_count
        for platx2,platy2,platw,plath in self.arena_platforms:
            if platy2 + plath >= H: continue  # floor drawn separately as moon
            sx=int(platx2-cam)
            if -platw<sx<W:
                for i in range(3):
                    gy2=platy2+plath+i
                    if gy2<H: pyxel.line(sx+i,gy2,sx+platw-1-i,gy2,
                               [2,8,14][i] if (f//8)%2==0 else [14,2,8][i])
                pyxel.rect(sx,platy2,platw,plath,5)
                pyxel.line(sx,platy2,sx+platw-1,platy2,14)
                pyxel.line(sx,platy2+1,sx+platw-1,platy2+1,2)
        for mp in self.arena_moving_plats:
            sx=int(mp["x"]-cam); mw=mp["w"]
            if -mw<sx<W:
                for i in range(3):
                    pyxel.line(sx+i,mp["y"]+6+i,sx+mw-1-i,mp["y"]+6+i,[2,8,14][i])
                pyxel.rect(sx,mp["y"],mw,6,5)
                pyxel.line(sx,mp["y"],sx+mw-1,mp["y"],8)
                pyxel.line(sx,mp["y"]+1,sx+mw-1,mp["y"]+1,14)
                pyxel.pset(sx+2,mp["y"]+6,8 if f%4<2 else 9)
                pyxel.pset(sx+mw-3,mp["y"]+6,8 if f%4<2 else 9)
        # Solid floor — cam-relative, spans ARENA_W
        floor_sx = int(0 - cam)
        pyxel.rect(max(0,floor_sx), H-18, min(W, ARENA_W), 18, 5)
        if 0<=H-18<H: pyxel.line(max(0,floor_sx), H-18, min(W-1,floor_sx+ARENA_W-1), H-18, 6)
        for e in self.arena_enemies:
            if -16<int(e.x-cam)<W+16: e.draw(cam)
        if self.boss and -20<int(self.boss.x-cam)<W+20: self.boss.draw(cam)
        for shot in self.arena_shots:
            sx=int(shot["x"]-cam)
            if 0<sx<W:
                col=shot.get("col",8); r=2 if shot.get("dmg",1)>=3 else 2
                pyxel.circ(sx,int(shot["y"]),r,col); pyxel.pset(sx,int(shot["y"]),7)
        for shot in self.player_shots:
            sx=int(shot["x"]-cam)
            if 0<sx<W: pyxel.line(sx-4,int(shot["y"]),sx+4,int(shot["y"]),11); pyxel.pset(sx,int(shot["y"]),7)
        for pp in self.particles: pp.draw()
        self.player.draw(cam,void_mode=True)
        self._draw_hud(arena=True)
        if self.arena_intro_timer>0:
            f2=pyxel.frame_count
            if (f2//4)%2==0 or self.arena_intro_timer>80:
                pyxel.rect(58,64,140,22,0); pyxel.rectb(58,64,140,22,14)
                pyxel.text(76,69,"BOSS DIMENSION",14)
                pyxel.text(72,78,"FIGHT THE BOSS!",8)
        if self.state=="gameover": self._draw_gameover(arena=True)
        if self.state=="win":      self._draw_win()

    def _draw_portal(self):
        t=self.portal_timer; max_t=80; prog=(max_t-t)/max_t
        pyxel.cls(0); cam=self.cam_x; f=pyxel.frame_count
        for platx2,platy2,platw,plath in self.platforms:
            sx=int(platx2-cam)+int(math.sin(prog*12+platy2*0.1)*prog*6)
            if -platw<sx<W: pyxel.rect(sx,platy2,platw,plath,5 if prog<0.6 else 2)
        for _ in range(int(prog*28)):
            pyxel.pset(random.randint(0,W-1),random.randint(16,H-1),random.choice([2,8,14,1]))
        cx,cy=W//2,H//2; r=int(prog*prog*150)
        for ang in range(0,360,8):
            rad=math.radians(ang+f*10)
            rx=int(cx+r*math.cos(rad)); ry=int(cy+r*0.55*math.sin(rad))
            if 0<=rx<W and 0<=ry<H: pyxel.pset(rx,ry,[14,2,8][ang//8%3])
        if prog>0.75: pyxel.cls(2 if (f//3)%2==0 else 0)
        pyxel.text(W//2-44,H//2-4,"ENTERING BOSS DIMENSION...",14 if (f//4)%2==0 else 8)

    def _draw_arena_exit(self):
        t=self.arena_exit_timer; f=pyxel.frame_count
        pyxel.cls(0); self.void_field.draw(self.arena_cam_x)
        for pp in self.particles: pp.draw()
        if t<20: pyxel.cls(7 if (f//2)%2==0 else 14)
        pyxel.rect(W//2-52,H//2-14,104,32,0); pyxel.rectb(W//2-52,H//2-14,104,32,14)
        pyxel.text(W//2-30,H//2-8,"BOSS DEFEATED!",10)
        pyxel.text(W//2-26,H//2+2,f"+1500 SCORE",7)
        if self.level<3: pyxel.text(W//2-40,H//2+12,"RETURNING TO GALAXY...",13)

    def _draw_gameover(self, arena=False):
        col=14 if arena else 8
        pyxel.rect(68,50,120,40,0); pyxel.rectb(68,50,120,40,col)
        pyxel.text(96,58,"GAME OVER",col)
        pyxel.text(82,70,f"SCORE: {self.score}",7)
        pyxel.text(78,82,"R = RESTART",13)

    def _draw_win(self):
        pyxel.rect(56,46,144,54,0); pyxel.rectb(56,46,144,54,11)
        pyxel.text(90,52,"YOU WIN!",10)
        pyxel.text(70,63,f"FINAL SCORE: {self.score}",7)
        pyxel.text(74,74,"GALAXY SAVED!",11)
        pyxel.text(78,85,"R = RESTART",13)

    def _draw_hud(self, arena=False):
        p=self.player
        pyxel.rect(0,0,W,12,0)
        # HP (left)
        pyxel.text(2,3,"HP",7)
        for i in range(p.max_hp): pyxel.rect(16+i*8,3,6,5,8 if i<p.hp else 5)
        # Level (center)
        lbl="BOSS" if arena else f"LV{self.level}"
        lcol=14 if arena else 11
        pyxel.text(W//2-len(lbl)*2,3,lbl,lcol)
        # Score (right)
        sc=str(self.score)
        pyxel.text(W-4-len(sc)*4,3,sc,10)

    def _draw_title(self):
        f=pyxel.frame_count
        pyxel.text(W//2-48,20,"SPACE PLATFORMER",11)
        col=10 if (f//20)%2==0 else 9
        pyxel.text(W//2-38,34,"ENTER = START",col)
        # Controls — one per line, left-aligned block
        lines=[
            ("ARROWS  move",7),
            ("Z / UP  jump",7),
            ("S       dash (invincible)",7),
            ("C       grapple hook",7),
            ("X       shoot",7),
            ("Wall: slide + Z = wall jump",6),
            ("No floor — fall = death",8),
            ("Level 3 portal = boss fight",14),
        ]
        bx=18
        for i,(txt,c) in enumerate(lines):
            pyxel.text(bx,50+i*10,txt,c)


if __name__=="__main__":
    Game()