"""
HUB WORLD - Jahrmarkt Edition  (Pyxel 512x512)
Steuerung: Pfeiltasten/WASD bewegen, ENTER betreten, SPACE/OBEN schiessen, ESC zurueck
Minispiele: POOL | RAKETE | ZIRKUS | SCHIESSEN
"""
import pyxel, random, math
 
W,H,FPS = 512,512,30
BK=0;NV=1;PU=2;GR=3;BR=4;DK=5;GY=6;WH=7
RD=8;OR=9;YL=10;LG=11;BL=12;IN=13;PK=14;PE=15
 
S_LOBBY="lobby";S_WATER="water";S_SPACE="space"
S_DUCK="duck";S_CIRCUS="circus"
S_OVER="over";S_WIN="win";S_INTRO="intro"
 
FC = lambda: pyxel.frame_count
FLAG_COLORS = [RD,YL,BL,OR,PK,LG,PU]

# ── SOUND IDS ────────────────────────────────────────────────────────────
SND_SHOOT = 0
SND_HIT   = 1
SND_HURT  = 2
SND_WIN   = 3
SND_OVER  = 4
SND_SELECT= 5
SND_SPLASH= 6

def setup_sounds():
    # SPACE/ENTER = Schuss (kurzer hoher Piep)
    pyxel.sounds[SND_SHOOT].set(
        notes="C3E3", tones="PP", volumes="33", effects="FN", speed=7)
    # Treffer (kurzer aufsteigender Ton)
    pyxel.sounds[SND_HIT].set(
        notes="C3E3G3", tones="SSS", volumes="333", effects="NNN", speed=8)
    # Schaden/Leben verloren (tiefer kurzer Ton)
    pyxel.sounds[SND_HURT].set(
        notes="A1F1", tones="NN", volumes="33", effects="FN", speed=12)
    # Gewonnen (kleine Fanfare)
    pyxel.sounds[SND_WIN].set(
        notes="C3E3G3C4E4G4C5", tones="TTTTTTT", volumes="4444444", effects="NNNNNNN", speed=10)
    # Game Over (absteigend, dumpf)
    pyxel.sounds[SND_OVER].set(
        notes="G2E2C2G1C1", tones="TTTTT", volumes="44444", effects="FFFFN", speed=14)
    # Portal/Auswahl betreten (Bestaetigungston)
    pyxel.sounds[SND_SELECT].set(
        notes="C3G3C4", tones="PPP", volumes="333", effects="NNN", speed=9)
    # Plansch (fuer Pool)
    pyxel.sounds[SND_SPLASH].set(
        notes="C2C1", tones="NN", volumes="33", effects="FN", speed=15)

 
def overlap(ax,ay,aw,ah,bx,by,bw,bh):
    # Kein < > um Browser-Encoding-Probleme zu vermeiden
    if ax+aw <= bx: return False
    if bx+bw <= ax: return False
    if ay+ah <= by: return False
    if by+bh <= ay: return False
    return True
 
def _portal_positions():
    cx,cy,r = 256,270,170
    angles = [-90,-90+90,-90+180,-90+270]
    specs = [
        (S_WATER, BL,  "POOL"),
        (S_SPACE, NV,  "RAKETE"),
        (S_CIRCUS,OR,  "ZIRKUS"),
        (S_DUCK,  GR,  "SCHIESSEN"),
    ]
    out = []
    for i,(sid,col,label) in enumerate(specs):
        a = math.radians(angles[i])
        x = int(cx+math.cos(a)*r)-32
        y = int(cy+math.sin(a)*r)-28
        out.append({"id":sid,"x":x,"y":y,"w":64,"h":56,"col":col,"label":label})
    return out
 
PORTALS = _portal_positions()
 
def _flags(y,spacing=26):
    for i in range(0,W+spacing,spacing):
        col = FLAG_COLORS[(i//spacing)%len(FLAG_COLORS)]
        x = i+int(math.sin(FC()*0.04+i*0.25)*3)
        pyxel.tri(x,y,x-6,y+11,x+6,y+11,col)
    pyxel.line(0,y,W,y,WH)
 
def _clown(x,y):
    # Grosse Schuhe
    pyxel.rect(x-4,y+15,9,4,RD)
    pyxel.rect(x+9,y+15,9,4,RD)
    # Beine
    pyxel.rect(x+1,y+10,4,6,WH)
    pyxel.rect(x+9,y+10,4,6,WH)
    # Bunter Koerper
    pyxel.rect(x,y+6,14,6,BL)
    pyxel.rect(x,y+6,7,6,RD)
    # Kragen-Pompons
    pyxel.circ(x+2,y+6,2,YL)
    pyxel.circ(x+7,y+6,2,YL)
    pyxel.circ(x+12,y+6,2,YL)
    # Kopf
    pyxel.circ(x+7,y+2,6,PE)
    # Clownhut
    pyxel.rect(x+3,y-7,8,7,PU)
    pyxel.rect(x+1,y-8,12,3,PU)
    pyxel.circ(x+7,y-9,3,YL)
    # Augen
    pyxel.pset(x+5,y+1,BK)
    pyxel.pset(x+9,y+1,BK)
    # Rote Nase
    pyxel.circ(x+7,y+3,2,RD)
    # Laecheln
    pyxel.pset(x+4,y+6,BK)
    pyxel.pset(x+10,y+6,BK)
    pyxel.line(x+5,y+7,x+9,y+7,BK)
 
def _draw_portal_image(p):
    """Kleines Bild im Inneren jeder Bude"""
    x,y,sid = p["x"],p["y"],p["id"]
    # Innenflaeche
    ix,iy,iw,ih = x+4,y+4,56,44
    if sid == S_WATER:
        # Wasser + Hai
        pyxel.rect(ix,iy,iw,ih,BL)
        for row in range(ih//6):
            pyxel.rect(ix,iy+row*6,iw,3,NV if row%2==0 else BL)
        # Hai
        pyxel.rect(ix+10,iy+20,22,8,GY)
        pyxel.tri(ix+28,iy+12,ix+22,iy+20,ix+32,iy+20,WH)
        pyxel.pset(ix+30,iy+24,RD)
        # Wellen oben
        for wi in range(0,iw,8):
            off = int(math.sin(FC()*0.12+wi*0.4)*2)
            pyxel.rect(ix+wi,iy+off,4,4,LG)
    elif sid == S_SPACE:
        # Sternenhimmel + Rakete
        pyxel.rect(ix,iy,iw,ih,NV)
        for si in range(12):
            sx2 = (si*17+3)%iw+ix
            sy2 = (si*13+5)%ih+iy
            pyxel.pset(sx2,sy2,WH)
        # Rakete
        rx,ry = ix+22,iy+8
        pyxel.tri(rx+4,ry,rx,ry+10,rx+8,ry+10,WH)
        pyxel.rect(rx,ry+10,8,14,BL)
        pyxel.rect(rx+2,ry+12,4,5,IN)
        if FC()%6 < 3:
            pyxel.tri(rx+2,ry+24,rx+6,ry+24,rx+4,ry+30,OR)
        # Asteroid
        pyxel.circ(ix+44,iy+30,6,BR)
    elif sid == S_CIRCUS:
        # Zirkus-Hintergrund
        pyxel.rect(ix,iy,iw,ih,RD)
        for ci in range(0,iw,10): pyxel.rect(ix+ci,iy,5,ih,PK)
        # Fallende Baelle
        for bi in range(3):
            bx2 = ix+8+bi*18
            by2 = iy+10+int(math.sin(FC()*0.1+bi*2)*14)
            col = [YL,BL,OR][bi]
            pyxel.circ(bx2,by2,6,col)
            pyxel.circb(bx2,by2,6,WH)
        # Korb unten
        pyxel.rect(ix+16,iy+ih-10,24,8,BR)
        pyxel.rectb(ix+16,iy+ih-10,24,8,YL)
    elif sid == S_DUCK:
        # Schiessbuden-Hintergrund
        pyxel.rect(ix,iy,iw,ih,GR)
        pyxel.rect(ix,iy,iw,14,BL)
        # Zielscheibe
        pyxel.circ(ix+28,iy+28,12,WH)
        pyxel.circ(ix+28,iy+28,8,RD)
        pyxel.circ(ix+28,iy+28,4,WH)
        pyxel.circ(ix+28,iy+28,2,RD)
        # Ente links
        pyxel.circ(ix+8,iy+32,5,YL)
        pyxel.circ(ix+13,iy+29,4,OR)
        pyxel.rect(ix+16,iy+27,3,2,OR)
 
def _bude(p,scores,active=False):
    x,y,w,h,col = p["x"],p["y"],p["w"],p["h"],p["col"]
    pulse = int(math.sin(FC()*0.14)*2) if active else 0
    # Schatten
    pyxel.rect(x+4,y+4,w+10,h+20,DK)
    # Wand
    pyxel.rect(x-2,y,w+4,h+16,WH)
    # Dach
    dach_h = 18
    pyxel.rect(x-6,y-dach_h,w+12,dach_h,col)
    stripe = YL if col != YL else WH
    for i in range(0,w+12,10): pyxel.rect(x-6+i,y-dach_h,5,dach_h,stripe)
    for i in range(0,w+12,8):  pyxel.tri(x-6+i,y,x-2+i,y-dach_h//2,x+2+i,y,col)
    pyxel.rect(x-2,y+h+12,w+4,4,col)
    pyxel.rectb(x-2-pulse,y-pulse,w+4+pulse*2,h+16+pulse*2,YL if active else DK)
    # Portal-Bild statt Farbe
    _draw_portal_image(p)
    # Label
    lbl = p["label"]
    pyxel.text(x+w//2-len(lbl)*2,y+h+17,lbl,YL if active else WH)
    pyxel.text(x+w//2-10,y+h+26,f"HS:{scores[p['id']]}",GY)
    # Lichterkette
    for i in range(0,w+12,10):
        pyxel.circ(x-6+i,y-dach_h-3,2,FLAG_COLORS[((i//10)+FC()//6)%len(FLAG_COLORS)])
 
 
# ── INTRO / TITELBILD ──────────────────────────────────────────────────────
class Intro:
    def __init__(self):
        self.t = 0

    def update(self):
        self.t += 1
        if pyxel.btnp(pyxel.KEY_RETURN) or pyxel.btnp(pyxel.KEY_SPACE):
            pyxel.play(0,SND_SELECT)
            return S_LOBBY
        return S_INTRO

    def draw(self):
        # Himmel
        for i in range(8): pyxel.rect(0,i*8,W,8,NV if i < 4 else IN)
        # Wiese
        pyxel.rect(0,H-100,W,100,GR)
        # Zirkuszelt gross in der Mitte
        tx,ty = W//2,H//2-40
        pyxel.tri(tx-140,ty+100,tx,ty-100,tx+140,ty+100,RD)
        for i in range(-140,140,28):
            pyxel.tri(tx+i,ty+100,tx+i+14,ty+100-(100-abs(i)*100//140),tx+i+28,ty+100,YL if (i//28)%2==0 else PK)
        pyxel.rect(tx-140,ty+100,280,14,WH)
        pyxel.rectb(tx-140,ty+100,280,14,DK)
        for i in range(-140,140,20):
            pyxel.rect(tx+i,ty+100,10,14,RD if (i//20)%2==0 else YL)
        pyxel.circ(tx,ty-100,5,YL)
        for ii in range(4):
            a = ii*math.pi/2+FC()*0.05
            pyxel.line(tx,ty-100,tx+int(math.cos(a)*10),ty-100+int(math.sin(a)*10),YL)

        # Riesenrad links
        cx,cy,rr = 90,260,55
        pyxel.rect(cx-3,cy+rr,6,60,BR)
        pyxel.circb(cx,cy,rr,YL); pyxel.circb(cx,cy,rr-2,OR)
        for i in range(8):
            a = i*math.pi/4+FC()*0.02
            pyxel.line(cx,cy,int(cx+math.cos(a)*rr),int(cy+math.sin(a)*rr),YL)
            gx = int(cx+math.cos(a)*rr)-4; gy = int(cy+math.sin(a)*rr)-4
            pyxel.rect(gx,gy,8,8,FLAG_COLORS[i%len(FLAG_COLORS)])
        pyxel.circ(cx,cy,5,WH)

        # Karussell rechts
        kx,ky = 422,265
        pyxel.rect(kx-3,ky+15,6,50,BR)
        pyxel.tri(kx-26,ky+15,kx,ky-12,kx+26,ky+15,PU)
        for i in range(5):
            a = i*math.pi*2/5+FC()*0.03
            hx = int(kx+math.cos(a)*22); hy = int(ky+15+math.sin(a)*6)
            pyxel.line(kx,ky+13,hx,hy+6,GY)
            pyxel.rect(hx-3,hy,7,9,FLAG_COLORS[i%len(FLAG_COLORS)])

        # Luftballons
        for i in range(6):
            bx2 = 40+i*82+int(math.sin(FC()*0.03+i*1.2)*6)
            by2 = 60+int(math.cos(FC()*0.04+i)*8)
            pyxel.circ(bx2,by2,7,FLAG_COLORS[i%len(FLAG_COLORS)])
            pyxel.line(bx2,by2+7,bx2,by2+20,GY)

        # Clowns
        _clown(120,H-46)
        _clown(360,H-46)

        # Titel
        title = "JAHRMARKT"
        pulse = 1 if (FC()//8)%2==0 else 0
        for i,ch in enumerate(title):
            pyxel.text(W//2-len(title)*4+i*8,90+pulse,ch,FLAG_COLORS[i%len(FLAG_COLORS)])
        sub = "HUB WORLD EDITION"
        pyxel.text(W//2-len(sub)*2,108,sub,WH)

        # Blinkender Hinweis
        if (FC()//15)%2==0:
            msg = "ENTER / SPACE druecken zum Start"
            pyxel.text(W//2-len(msg)*2,H-20,msg,YL)


# ── LOBBY ──────────────────────────────────────────────────────────────────
class Lobby:
    def __init__(self,scores):
        self.scores = scores
        self.px = W//2-7; self.py = 270; self.ap = None
 
    def update(self):
        sp = 3
        if pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A): self.px -= sp
        if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D): self.px += sp
        if pyxel.btn(pyxel.KEY_UP)    or pyxel.btn(pyxel.KEY_W): self.py -= sp
        if pyxel.btn(pyxel.KEY_DOWN)  or pyxel.btn(pyxel.KEY_S): self.py += sp
        self.px = max(0,min(W-14,self.px))
        self.py = max(50,min(H-20,self.py))
        self.ap = None
        for p in PORTALS:
            if overlap(self.px,self.py,14,14,p["x"],p["y"],p["w"],p["h"]):
                self.ap = p; break
        if self.ap and pyxel.btnp(pyxel.KEY_RETURN):
            pyxel.play(0,SND_SELECT)
            return self.ap["id"]
        return S_LOBBY
 
    def draw(self):
        # Hintergrund - alles gruene Wiese
        pyxel.rect(0,0,W,H,GR)
 
        # RIESENRAD (rechts)
        cx,cy,rr = 420,195,65
        pyxel.rect(cx-4,cy+rr,8,80,BR)
        pyxel.rect(cx-30,cy+rr+75,60,8,BR)
        pyxel.circb(cx,cy,rr,YL); pyxel.circb(cx,cy,rr-2,OR)
        for i in range(10):
            a = i*math.pi/5+FC()*0.018
            pyxel.line(cx,cy,int(cx+math.cos(a)*rr),int(cy+math.sin(a)*rr),YL)
            gx = int(cx+math.cos(a)*rr)-5; gy = int(cy+math.sin(a)*rr)-5
            pyxel.rect(gx,gy,10,10,FLAG_COLORS[i%len(FLAG_COLORS)])
            pyxel.rectb(gx,gy,10,10,WH)
        pyxel.circ(cx,cy,7,WH); pyxel.circ(cx,cy,4,YL)
 
        # KARUSSELL (links)
        kx,ky = 75,210
        pyxel.rect(kx-3,ky+18,6,55,BR)
        pyxel.rect(kx-22,ky+68,44,8,BR)
        pyxel.tri(kx-30,ky+20,kx,ky-10,kx+30,ky+20,RD)
        pyxel.tri(kx-20,ky+20,kx,ky,kx+20,ky+20,YL)
        for i in range(6):
            a = i*math.pi/3+FC()*0.025
            hx = int(kx+math.cos(a)*28); hy = int(ky+20+math.sin(a)*8)
            pyxel.line(kx,ky+18,hx,hy+8,BR)
            pyxel.rect(hx-4,hy,9,12,FLAG_COLORS[i%len(FLAG_COLORS)])
            pyxel.rectb(hx-4,hy,9,12,WH)
        pyxel.circ(kx,ky-10,5,YL)
 
        # SCHAUKEL (mitte-links)
        sx,sy = 168,100
        pyxel.rect(sx-2,sy,4,70,BR); pyxel.rect(sx+74,sy,4,70,BR)
        pyxel.rect(sx-2,sy,80,5,BR)
        for i in range(4):
            ox = sx+10+i*18
            sw = int(math.sin(FC()*0.06+i*1.1)*12)
            pyxel.line(ox,sy+5,ox+sw,sy+45,GY)
            pyxel.line(ox+8,sy+5,ox+8+sw,sy+45,GY)
            pyxel.rect(ox-2+sw,sy+43,14,5,BR)
            pyxel.circ(ox+4+sw,sy+40,4,FLAG_COLORS[i%len(FLAG_COLORS)])
 
        # ZUCKERWATTE-STAND
        zx,zy = 230,340
        pyxel.rect(zx,zy,40,28,YL); pyxel.rectb(zx,zy,40,28,BR)
        pyxel.text(zx+3,zy+10,"Zucker",RD)
        for i in range(3):
            wx2 = zx+5+i*13
            wy2 = zy-18+int(math.sin(FC()*0.05+i)*3)
            pyxel.circ(wx2,wy2,8,PK); pyxel.circ(wx2+5,wy2-3,6,PK)
            pyxel.rect(wx2+1,wy2,3,18,BR)
 
        # LUFTBALLONS
        for i in range(5):
            bx2 = 30+i*90+int(math.sin(FC()*0.03+i*1.3)*8)
            by2 = 120+int(math.cos(FC()*0.04+i)*12)
            col = FLAG_COLORS[i%len(FLAG_COLORS)]
            pyxel.circ(bx2,by2,8,col)
            pyxel.line(bx2,by2+8,bx2-3+int(math.sin(FC()*0.05+i)*4),by2+25,GY)
 
        # LOSBUDE
        lx,ly = 340,345
        pyxel.rect(lx,ly,55,32,WH); pyxel.rectb(lx,ly,55,32,OR)
        pyxel.rect(lx,ly-14,55,14,OR)
        for i in range(0,55,10): pyxel.rect(lx+i,ly-14,5,14,YL)
        pyxel.text(lx+6,ly+5,"LOSE!",RD)
        pyxel.text(lx+6,ly+16,"Gl.rad",GY)
 
        # Lichterboegen zwischen Buden
        for i in range(len(PORTALS)):
            p1 = PORTALS[i]; p2 = PORTALS[(i+1)%len(PORTALS)]
            x1 = p1["x"]+p1["w"]//2; y1 = p1["y"]+p1["h"]//2
            x2 = p2["x"]+p2["w"]//2; y2 = p2["y"]+p2["h"]//2
            for s in range(12):
                t = s/12
                bx2 = int(x1+(x2-x1)*t)
                by2 = int(y1+(y2-y1)*t-math.sin(t*math.pi)*30)
                pyxel.circ(bx2,by2,2,FLAG_COLORS[(s+FC()//8)%len(FLAG_COLORS)])
 
        for p in PORTALS: _bude(p,self.scores,active=(p is self.ap))
        _clown(self.px,self.py)
 
        pyxel.rect(0,H-22,W,22,BK)
        msg = ">>> ENTER: Bude betreten! <<<" if self.ap else "Laufe zu einer Jahrmarktbude!"
        pyxel.text(W//2-len(msg)*2,H-16,msg,YL if self.ap else WH)
        pyxel.text(4,H-8,"ESC=Ende",GY)
 
 
# ── POOL ───────────────────────────────────────────────────────────────────
class WaterGame:
    def __init__(self):
        self.px = W//2; self.py = H-40
        self.sharks = []
        for _ in range(8):
            dx = random.uniform(2,4)
            if random.random() < 0.5: dx = -dx
            self.sharks.append({"x":random.randint(0,W-28),
                                 "y":random.randint(-H,-20),
                                 "dx":dx,
                                 "phase":random.uniform(0,6)})
        self.bubbles = [{"x":random.randint(0,W),"y":random.randint(0,H)} for _ in range(30)]
        self.timer = FPS*22; self.score = 0
 
    def update(self):
        if pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A): self.px -= 3
        if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D): self.px += 3
        if pyxel.btn(pyxel.KEY_UP)    or pyxel.btn(pyxel.KEY_W): self.py -= 4
        else: self.py += 1.4
        if pyxel.btnp(pyxel.KEY_UP) or pyxel.btnp(pyxel.KEY_W): pyxel.play(0,SND_SPLASH)
        self.px = max(0,min(W-14,int(self.px)))
        self.py = max(0,min(H-14,self.py))
        t = self.timer
        for sh in self.sharks:
            sp = 1.0+((FPS*22-t)/(FPS*22))*1.5
            sh["y"] += 2.2*sp
            sh["x"] += sh["dx"]+math.sin(t*0.05+sh["phase"])*1.5
            if sh["x"] < -28: sh["x"] = -28
            if sh["x"] > W:   sh["x"] = W
            if sh["y"] > H:
                sh["y"] = random.randint(-120,-20)
                sh["x"] = random.randint(0,W-28)
            if overlap(int(self.px),int(self.py),14,14,int(sh["x"]),int(sh["y"]),28,12):
                pyxel.play(0,SND_OVER)
                return S_OVER,self.score
        for b in self.bubbles:
            b["y"] -= 0.6
            if b["y"] < 0: b["y"] = H; b["x"] = random.randint(0,W)
        self.timer -= 1
        self.score = (FPS*22-self.timer)//FPS
        if self.py < 18:
            pyxel.play(0,SND_WIN)
            return S_WIN,self.score
        if self.timer <= 0:
            pyxel.play(0,SND_OVER)
            return S_OVER,self.score
        return S_WATER,self.score
 
    def draw(self):
        pyxel.rect(0,0,W,H,NV)
        for i in range(H//20): pyxel.rect(0,i*20,W,10,BL if i%2==0 else NV)
        for b in self.bubbles:
            pyxel.circb(int(b["x"]),int(b["y"]),3,BL)
            pyxel.pset(int(b["x"]),int(b["y"]),WH)
        for i in range(0,W,14):
            pyxel.rect(i,18+int(math.sin(FC()*0.12+i*0.25)*5),14,12,LG)
        pyxel.text(W//2-48,4,"~ OBERFLAECHE - ZIEL! ~",WH)
        for sh in self.sharks:
            sx,sy = int(sh["x"]),int(sh["y"])
            pyxel.rect(sx,sy,28,12,GY)
            pyxel.tri(sx+20,sy-10,sx+14,sy,sx+24,sy,WH)
            pyxel.pset(sx+22,sy+5,RD)
            pyxel.tri(sx,sy+6,sx-8,sy+4,sx-8,sy+10,GY)
        _clown(int(self.px),int(self.py))
        pyxel.rect(0,H-22,W,22,NV)
        t = self.timer//FPS
        col = RD if t < 8 else (YL if t < 14 else WH)
        pyxel.text(4,H-18,f"Zeit:{t}s",col)
        pyxel.text(80,H-18,f"Punkte:{self.score}",WH)
        pyxel.text(200,H-18,"W=Schwimmen!",GY)
 
 
# ── RAKETE ─────────────────────────────────────────────────────────────────
class SpaceGame:
    def __init__(self):
        self.px = W//2-8; self.py = H-36
        self.bullets = []; self.asts = []
        self.stars = []
        for _ in range(70):
            self.stars.append({"x":random.randint(0,W),"y":random.randint(0,H),"sp":random.uniform(0.5,1.8)})
        self.score = 0; self.lives = 3; self.cd = 0; self.t = 0
 
    def update(self):
        if pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A): self.px -= 3
        if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D): self.px += 3
        self.px = max(0,min(W-16,self.px))
        self.cd -= 1
        fire = pyxel.btn(pyxel.KEY_SPACE) or pyxel.btn(pyxel.KEY_UP)
        if fire and self.cd <= 0:
            self.bullets.append({"x":self.px+7,"y":self.py}); self.cd = 7
            pyxel.play(0,SND_SHOOT)
        new_b = []
        for b in self.bullets:
            b["y"] -= 7
            if b["y"] > 0: new_b.append(b)
        self.bullets = new_b
        for st in self.stars:
            st["y"] += st["sp"]
            if st["y"] > H: st["y"] = 0; st["x"] = random.randint(0,W)
        self.t += 1
        if self.t % max(10,32-self.score//2) == 0:
            self.asts.append({"x":random.randint(0,W-14),"y":-14,
                              "vy":random.uniform(1.8,3.5),"r":random.randint(6,13)})
        for a in self.asts: a["y"] += a["vy"]
        hit_a = set(); hit_b = set()
        for i,a in enumerate(self.asts):
            for j,b in enumerate(self.bullets):
                if overlap(int(b["x"])-2,int(b["y"])-2,4,4,
                           int(a["x"])-a["r"],int(a["y"])-a["r"],a["r"]*2,a["r"]*2):
                    hit_a.add(i); hit_b.add(j); self.score += 1
        self.asts    = [a for i,a in enumerate(self.asts)    if i not in hit_a]
        self.bullets = [b for i,b in enumerate(self.bullets) if i not in hit_b]
        if hit_a: pyxel.play(0,SND_HIT)
        for a in list(self.asts):
            if overlap(self.px,self.py,16,18,
                       int(a["x"])-a["r"],int(a["y"])-a["r"],a["r"]*2,a["r"]*2):
                self.lives -= 1; self.asts.remove(a)
                pyxel.play(0,SND_HURT)
                if self.lives <= 0:
                    pyxel.play(0,SND_OVER)
                    return S_OVER,self.score
                break
        self.asts = [a for a in self.asts if a["y"] < H]
        if self.score >= 20:
            pyxel.play(0,SND_WIN)
            return S_WIN,self.score
        return S_SPACE,self.score
 
    def draw(self):
        pyxel.rect(0,0,W,H,NV)
        for st in self.stars:
            pyxel.pset(int(st["x"]),int(st["y"]),WH if st["sp"] > 1.2 else GY)
        for a in self.asts:
            pyxel.circ(int(a["x"]),int(a["y"]),a["r"],BR)
            pyxel.circb(int(a["x"]),int(a["y"]),a["r"],GY)
        for b in self.bullets: pyxel.rect(int(b["x"]),int(b["y"]),3,8,YL)
        x,y = self.px,self.py
        pyxel.tri(x+8,y,x,y+18,x+16,y+18,BL)
        pyxel.tri(x+8,y+8,x+4,y+18,x+12,y+18,IN)
        pyxel.pset(x+8,y+10,WH)
        if FC()%4 < 2: pyxel.tri(x+5,y+18,x+11,y+18,x+8,y+28,OR)
        pyxel.rect(0,0,W,18,NV)
        lives_str = "v" * self.lives
        pyxel.text(4,5,f"Score:{self.score}/20  Leben:{lives_str}  SPC=Feuer",WH)
 
 
# ── SCHIESSEN ──────────────────────────────────────────────────────────────
class DuckGame:
    def __init__(self):
        self.bullets = []; self.ducks = []
        self.score = 0; self.lives = 5
        self.t = 0; self.cd = 0; self.cx = W//2
 
    def update(self):
        if pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A): self.cx = max(20,self.cx-4)
        if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D): self.cx = min(W-20,self.cx+4)
        self.cd -= 1
        fire = pyxel.btn(pyxel.KEY_SPACE) or pyxel.btn(pyxel.KEY_UP)
        if fire and self.cd <= 0:
            self.bullets.append({"x":self.cx,"y":H-50}); self.cd = 10
            pyxel.play(0,SND_SHOOT)
        new_b = []
        for b in self.bullets:
            b["y"] -= 8
            if b["y"] > 0: new_b.append(b)
        self.bullets = new_b
        self.t += 1
        interval = max(18,55-self.score*2)
        if self.t % interval == 0:
            fl = random.random() < 0.5
            spd = random.uniform(2,4)
            self.ducks.append({
                "x":-30 if fl else W+10,
                "y":60+random.randint(0,3)*90,
                "dx":spd if fl else -spd,
                "dy":random.uniform(-0.3,0.3),
                "alive":True,"dead_t":0})
        for d in self.ducks:
            if d["alive"]: d["x"] += d["dx"]; d["y"] += d["dy"]
            else: d["dead_t"] += 1
        for d in list(self.ducks):
            if not d["alive"]: continue
            for b in list(self.bullets):
                if overlap(int(b["x"])-2,int(b["y"])-2,4,4,
                           int(d["x"])-10,int(d["y"])-8,24,16):
                    d["alive"] = False; self.score += 1
                    pyxel.play(0,SND_HIT)
                    if b in self.bullets: self.bullets.remove(b)
                    break
        missed = [d for d in self.ducks if d["alive"] and (d["x"] < -60 or d["x"] > W+60)]
        self.lives -= len(missed)
        if missed: pyxel.play(0,SND_HURT)
        self.ducks = [d for d in self.ducks
                      if not (d["alive"] and (d["x"] < -60 or d["x"] > W+60))]
        self.ducks = [d for d in self.ducks if d["dead_t"] < 15]
        if self.lives <= 0:
            pyxel.play(0,SND_OVER)
            return S_OVER,self.score
        if self.score >= 15:
            pyxel.play(0,SND_WIN)
            return S_WIN,self.score
        return S_DUCK,self.score
 
    def draw(self):
        pyxel.rect(0,0,W,H,GR); pyxel.rect(0,0,W,50,BL)
        for i in range(0,W,30):
            pyxel.circ(i+int(math.sin(FC()*0.02+i)*3),20,10,WH)
        for xp in [80,200,320,440]:
            pyxel.circ(xp,130,14,RD); pyxel.circ(xp,130,9,WH); pyxel.circ(xp,130,5,RD)
        pyxel.rect(0,55,W,8,BR)
        for row in range(4): pyxel.line(0,60+row*90,W,60+row*90,BR)
        for d in self.ducks:
            dx,dy = int(d["x"]),int(d["y"])
            if d["alive"]:
                pyxel.circ(dx,dy,8,YL)
                hx = dx+(6 if d["dx"] > 0 else -6)
                pyxel.circ(hx,dy-4,5,OR)
                bx2 = dx+(8 if d["dx"] > 0 else -12)
                pyxel.rect(bx2,dy-6,5,3,OR)
                ex = dx+(7 if d["dx"] > 0 else -5)
                pyxel.pset(ex,dy-5,BK)
            else:
                for ii in range(4):
                    a = ii*math.pi/2+d["dead_t"]*0.2
                    pyxel.pset(dx+int(math.cos(a)*8),dy+int(math.sin(a)*8),YL)
        for b in self.bullets: pyxel.rect(int(b["x"])-1,int(b["y"])-4,3,8,WH)
        pyxel.rect(self.cx-14,H-40,28,28,GY)
        pyxel.rect(self.cx-3,H-52,6,16,DK)
        pyxel.rectb(self.cx-14,H-40,28,28,WH)
        pyxel.rect(0,H-22,W,22,BK)
        lives_str = "|" * self.lives
        pyxel.text(4,H-18,f"Score:{self.score}/15  Leben:{lives_str}  SPC=Schuss",WH)
 
 
# ── ZIRKUS ─────────────────────────────────────────────────────────────────
class CircusGame:
    def __init__(self):
        self.px = W//2-12; self.py = H-50
        self.balls = []; self.score = 0; self.lives = 3; self.t = 0
 
    def update(self):
        if pyxel.btn(pyxel.KEY_LEFT)  or pyxel.btn(pyxel.KEY_A): self.px -= 5
        if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D): self.px += 5
        self.px = max(0,min(W-28,self.px))
        self.t += 1
        if self.t % max(18,48-self.score//3) == 0:
            col = random.choice([RD,YL,BL,OR,PK,LG])
            self.balls.append({"x":random.randint(10,W-20),"y":-10,
                               "vy":random.uniform(2,5),"col":col,"good":True})
        if self.t % 55 == 0:
            self.balls.append({"x":random.randint(10,W-20),"y":-10,
                               "vy":random.uniform(2,4),"col":DK,"good":False})
        for b in self.balls: b["y"] += b["vy"]
        missed = [b for b in self.balls if b["y"] > H and b["good"]]
        self.balls = [b for b in self.balls if b["y"] < H]
        self.lives -= len(missed)
        if missed: pyxel.play(0,SND_HURT)
        for b in list(self.balls):
            if overlap(self.px,self.py,28,14,int(b["x"])-10,int(b["y"])-10,20,20):
                if b["good"]:
                    self.score += 1
                    pyxel.play(0,SND_HIT)
                else:
                    self.lives -= 1
                    pyxel.play(0,SND_HURT)
                self.balls.remove(b)
        if self.lives <= 0:
            pyxel.play(0,SND_OVER)
            return S_OVER,self.score
        if self.score >= 25:
            pyxel.play(0,SND_WIN)
            return S_WIN,self.score
        return S_CIRCUS,self.score
 
    def draw(self):
        pyxel.rect(0,0,W,H,RD)
        for i in range(0,W,36): pyxel.rect(i,0,18,H,PK)
        _flags(8,18)
        for i,sx in enumerate([80,256,430]):
            a = math.sin(FC()*0.04+i*2)*0.4
            ex = int(sx+math.sin(a)*100)
            pyxel.line(sx,0,ex,H-60,YL)
        for b in self.balls:
            pyxel.circ(int(b["x"]),int(b["y"]),10,b["col"])
            pyxel.circb(int(b["x"]),int(b["y"]),10,WH)
            lbl = "X" if not b["good"] else "o"
            pyxel.text(int(b["x"])-3,int(b["y"])-3,lbl,WH)
        pyxel.rect(self.px,self.py,28,14,BR)
        pyxel.rectb(self.px,self.py,28,14,YL)
        pyxel.text(self.px+6,self.py+3,"[]",WH)
        pyxel.rect(0,H-22,W,22,BK)
        lives_str = "o" * self.lives
        pyxel.text(4,H-18,f"Score:{self.score}/25  Leben:{lives_str}  X=SCHLECHT",WH)
 
 
# ── GAME CONTROLLER ────────────────────────────────────────────────────────
class Game:
    def __init__(self):
        pyxel.init(W,H,title="Jahrmarkt Hub World",fps=FPS)
        setup_sounds()
        self.scores = {S_WATER:0,S_SPACE:0,S_DUCK:0,S_CIRCUS:0}
        self.state = S_INTRO
        self.scene = Intro()
        pyxel.run(self.update,self.draw)
 
    def _enter(self,sid):
        self.state = sid
        if   sid == S_INTRO:  self.scene = Intro()
        elif sid == S_LOBBY:  self.scene = Lobby(self.scores)
        elif sid == S_WATER:  self.scene = WaterGame()
        elif sid == S_SPACE:  self.scene = SpaceGame()
        elif sid == S_DUCK:   self.scene = DuckGame()
        elif sid == S_CIRCUS: self.scene = CircusGame()
 
    def update(self):
        if pyxel.btnp(pyxel.KEY_ESCAPE):
            if self.state != S_LOBBY: self._enter(S_LOBBY)
            else: pyxel.quit()
            return
        if self.state in (S_OVER,S_WIN):
            if pyxel.btnp(pyxel.KEY_RETURN): self._enter(S_LOBBY)
            return
        result = self.scene.update()
        if isinstance(result,tuple):
            new_state,score = result
            self.scores[self.state] = max(self.scores.get(self.state,0),score)
            if new_state in (S_OVER,S_WIN):
                self.state = new_state
            elif new_state != self.state:
                self._enter(new_state)
        elif result != self.state:
            self._enter(result)
 
    def draw(self):
        pyxel.cls(BK)
        if self.state in (S_OVER,S_WIN): self._drw_end()
        else: self.scene.draw()
 
    def _drw_end(self):
        win = self.state == S_WIN
        pyxel.rect(60,160,392,180,NV if win else BK)
        pyxel.rectb(60,160,392,180,YL if win else RD)
        pyxel.rect(64,164,384,34,OR if win else RD)
        for i in range(0,390,26):
            pyxel.tri(60+i,164,60+i+13,198,60+i+26,164,PK if win else YL)
        msg = "LEVEL GESCHAFFT!" if win else "GAME OVER"
        pyxel.text(W//2-(len(msg)*4)//2,170,msg,YL if win else WH)
        if win:
            for i in range(8): pyxel.circ(80+i*46,200,5,FLAG_COLORS[i%len(FLAG_COLORS)])
        pyxel.text(70,210,"Highscores:",YL)
        pyxel.text(70,224,f"Pool:{self.scores[S_WATER]}  Rakete:{self.scores[S_SPACE]}",WH)
        pyxel.text(70,238,f"Zirkus:{self.scores[S_CIRCUS]}  Schiessen:{self.scores[S_DUCK]}",WH)
        pyxel.text(70,268,"ENTER: Zurueck zur Lobby",GY)
 
if __name__ == "__main__":
    Game()
