# ================================================================
#  KUNG FU RUN  v7  –  Neues Punktesystem + Highscore + Hund
#  Steuerung:  A/D oder Pfeiltasten = Laufen
#              LEERTASTE / PFEIL-OBEN = Springen / Hund befreien
#              SHIFT = Chi-Boost
#              R = Neustart
#  Ninjas toeten: von OBEN auf sie draufspringen!
#  Ziel: nach 2000 Pixeln den Hund im Käfig befreien!
# ================================================================
 
import pyxel
import math
import random
 
# ----------------------------------------------------------------
# KONSTANTEN
# ----------------------------------------------------------------
SW, SH = 256, 192
 
GRAV          = 0.38
JFORCE        = -7.2
RSPEED        = 2.0
BSPEED        = 4.0
STOMP_BOUNCE  = -5.8
 
SHURIKEN_SPEED    = 2.8
SHURIKEN_COOLDOWN = 120
THROW_RANGE       = 130
 
GOAL_DISTANCE = 10000   # Pixel bis zum Ziel
 
S_CUT  = 0
S_PLAY = 1
S_WIN  = 2
S_LOSE = 3
 
BK=0; NV=1; PU=2; GN=3; BR=4; DG=5; LG=6; WH=7
RD=8; OR=9; YL=10; LN=11; BL=12; LB=13; PK=14; PE=15
 
 
# ================================================================
# ZEICHENFUNKTIONEN
# ================================================================
 
def draw_player(x, y, fr, facing, boosting):
    bx = int(x); by = int(y)
    step = (fr // 7) % 2
    pyxel.rect(bx-2, by+1, 13, 1, YL)
    pyxel.rect(bx-1, by,   11, 1, YL)
    pyxel.rect(bx+1, by-1,  7, 2, OR)
    pyxel.rect(bx+2, by-2,  5, 1, YL)
    pyxel.rect(bx+2, by+1,  5, 1, BR)
    pyxel.rect(bx+1, by+2, 7, 5, PE)
    eye_x = bx+5 if facing==1 else bx+2
    pyxel.pset(eye_x, by+4, BK)
    pyxel.pset(bx+3, by+6, BR)
    pyxel.pset(bx+5, by+6, BR)
    if facing == 1:
        pyxel.pset(bx+8, by+2, BK); pyxel.pset(bx+8, by+3, BK)
    else:
        pyxel.pset(bx, by+2, BK); pyxel.pset(bx, by+3, BK)
    pyxel.rect(bx+1, by+7, 7, 5, BL)
    pyxel.pset(bx+1, by+7, NV); pyxel.pset(bx+7, by+7, NV)
    pyxel.pset(bx+4, by+8, WH); pyxel.pset(bx+4, by+10, WH)
    pyxel.rect(bx+1, by+12, 7, 1, BR)
    pyxel.pset(bx+4, by+12, YL)
    pyxel.rect(bx-1, by+8+step, 2, 3, PE)
    pyxel.rect(bx+8, by+8+(1-step), 2, 3, PE)
    kx = bx+10 if facing==1 else bx-2
    pyxel.pset(kx, by+9+step, YL); pyxel.pset(kx+1, by+9+step, YL)
    pyxel.pset(kx, by+8+step, OR)
    pyxel.rect(bx+1, by+13, 3, 3, NV)
    pyxel.rect(bx+5, by+13+step, 3, 3, NV)
    pyxel.rect(bx,   by+15, 4, 1, BK)
    pyxel.rect(bx+4, by+15+step, 4, 1, BK)
    if boosting:
        pyxel.rectb(bx-1, by-1, 11, 18, YL)
        if fr % 6 < 3: pyxel.rectb(bx-2, by-2, 13, 20, OR)
 
 
def draw_ninja(x, y, fr, facing, dying=False, die_t=0, is_thrower=False):
    bx = int(x); by = int(y)
    if dying:
        sp = min(die_t * 2, 10)
        pyxel.rect(bx-sp, by+3, 5, 5, DG)
        pyxel.rect(bx+4+sp, by+3, 5, 5, DG)
        pyxel.circ(bx+2+sp//3, by, 3, BK)
        for i in range(5):
            a = i * 1.26 + die_t * 0.2
            sx2 = bx + 4 + int(math.cos(a) * sp * 1.4)
            sy2 = by + 4 + int(math.sin(a) * sp * 1.4)
            pyxel.pset(sx2, sy2, WH if i%2==0 else YL)
        return
    leg = (fr // 6) % 2
    pyxel.rect(bx,   by,   9, 3, BK)
    pyxel.rect(bx-1, by+3, 11, 4, BK)
    if is_thrower: pyxel.line(bx, by+3, bx+10, by+3, RD)
    eye_col = YL if is_thrower else RD
    pyxel.pset(bx+1, by+5, eye_col); pyxel.pset(bx+7, by+5, eye_col)
    pyxel.pset(bx+2, by+4, eye_col); pyxel.pset(bx+6, by+4, eye_col)
    pyxel.pset(bx+3, by+6, DG); pyxel.pset(bx+5, by+6, DG)
    body_col = PU if is_thrower else DG
    pyxel.rect(bx,   by+7, 9, 5, body_col)
    pyxel.rect(bx+1, by+8, 7, 2, LG)
    pyxel.rect(bx-1, by+7, 2, 4, BK)
    pyxel.rect(bx+8, by+7, 2, 4, BK)
    pyxel.rect(bx, by+12, 9, 1, WH)
    pyxel.pset(bx+4, by+12, YL)
    pyxel.rect(bx,   by+13, 4, 4, BK)
    pyxel.rect(bx+5, by+13+leg, 4, 3, BK)
    pyxel.rect(bx-1, by+16, 5, 1, DG)
    pyxel.rect(bx+4, by+16+leg, 5, 1, DG)
    pyxel.rect(bx-2, by+8+leg, 2, 4, DG)
    pyxel.rect(bx+9, by+8,     2, 4, DG)
    if is_thrower:
        wx2 = bx+12 if facing == 1 else bx-4
        wy2 = by+8
        rot = (fr // 4) % 4
        if rot == 0:
            pyxel.line(wx2, wy2-2, wx2, wy2+2, LG)
            pyxel.line(wx2-2, wy2, wx2+2, wy2, LG)
        else:
            pyxel.line(wx2, wy2-2, wx2, wy2+2, WH)
            pyxel.line(wx2-2, wy2, wx2+2, wy2, WH)
        pyxel.pset(wx2, wy2, YL)
    else:
        if facing == 1:
            pyxel.line(bx+10, by+6, bx+15, by+2, LG)
            pyxel.pset(bx+11, by+7, WH)
            pyxel.rect(bx+9,  by+7, 2, 2, BR)
        else:
            pyxel.line(bx-1, by+6, bx-6, by+2, LG)
            pyxel.pset(bx-2, by+7, WH)
            pyxel.rect(bx-1, by+7, 2, 2, BR)
 
 
def draw_shuriken(x, y, fr):
    bx, by = int(x), int(y)
    rot = (fr // 2) % 4
    if rot % 2 == 0:
        pyxel.line(bx-3, by, bx+3, by, LG)
        pyxel.line(bx, by-3, bx, by+3, LG)
    else:
        pyxel.line(bx-2, by-2, bx+2, by+2, LG)
        pyxel.line(bx+2, by-2, bx-2, by+2, LG)
    pyxel.pset(bx, by, YL)
 
 
def draw_spikes(x, y):
    bx, by = int(x), int(y)
    for i in range(3):
        ox = i * 5
        pyxel.tri(bx+ox, by+8, bx+ox+2, by, bx+ox+4, by+8, LG)
        pyxel.line(bx+ox, by+8, bx+ox+2, by, WH)
 
 
def draw_cannon(x, y, facing):
    bx, by = int(x), int(y)
    pyxel.rect(bx, by+4, 12, 6, DG)
    if facing == 1: pyxel.rect(bx+8, by+2, 6, 4, BK)
    else: pyxel.rect(bx-2, by+2, 6, 4, BK)
    pyxel.circ(bx+6, by+8, 3, BR)
 
 
def draw_coin(x, y, kind, fr):
    cx, cy = int(x) + 5, int(y) + 5
    pulse = 1 if (fr // 8) % 2 == 0 else 0
    pyxel.circ(cx, cy, 4+pulse, OR)
    pyxel.circ(cx, cy, 3, YL)
    pyxel.pset(cx, cy, BK)
 
 
def draw_dog_cage(x, y, freed=False, fr=0):
    """Grosser Hundekäfig mit Hund drin."""
    bx, by = int(x), int(y)
    CW, CH = 50, 38
    if freed:
        # Kaefig offen, Hund rennt
        pyxel.rectb(bx, by, CW, CH, BR)
        pyxel.rect(bx, by, CW//2, 3, BR)  # obere Haelfte Tuer weg
        # Hund rennt raus
        dx = bx + CW + 10 + (fr % 40)
        _draw_dog(dx, by + CH - 12, fr, running=True)
        return
    # Stabgitter
    pyxel.rectb(bx, by, CW, CH, BR)
    pyxel.rect(bx+1, by, CW-2, 3, BR)
    pyxel.rect(bx+1, by+CH-3, CW-2, 3, BR)
    for i in range(1, 9):
        pyxel.rect(bx + i*5 + 1, by+3, 2, CH-6, BR)
    # Schloss
    pyxel.rect(bx+CW//2-3, by+CH//2-3, 7, 6, YL)
    pyxel.rectb(bx+CW//2-2, by+CH//2-6, 5, 5, YL)
    # Dach-Ornament
    pyxel.rect(bx+CW//2-5, by-6, 10, 7, YL)
    pyxel.tri(bx+CW//2-7, by-6, bx+CW//2, by-12, bx+CW//2+7, by-6, OR)
    # Hund im Kaeig
    _draw_dog(bx + 8, by + CH - 12, fr, running=False)
    # SPACE-Hinweis
    pyxel.text(bx - 10, by - 20, "SPACE = Befreien!", YL)
 
 
def _draw_dog(x, y, fr, running=False):
    """Kleiner Hund (Pixel-Art)."""
    bx, by = int(x), int(y)
    step = (fr // 5) % 2 if running else 0
    # Koerper
    pyxel.rect(bx, by, 14, 7, BR)
    # Kopf
    pyxel.rect(bx+10, by-4, 8, 7, BR)
    # Schnauze
    pyxel.rect(bx+16, by-2, 4, 3, PE)
    pyxel.pset(bx+19, by-1, BK)   # Nase
    # Auge
    pyxel.pset(bx+13, by-3, BK)
    # Ohr
    pyxel.rect(bx+10, by-7, 4, 4, BR)
    pyxel.rect(bx+10, by-7, 3, 3, OR)
    # Schwanz
    pyxel.line(bx, by+1, bx-3, by-3, BR)
    pyxel.pset(bx-3, by-3, YL)
    # Beine
    if running:
        pyxel.rect(bx+2,  by+6, 2, 3+step,   BR)
        pyxel.rect(bx+6,  by+6, 2, 3+(1-step), BR)
        pyxel.rect(bx+10, by+6, 2, 3+step,   BR)
    else:
        pyxel.rect(bx+2,  by+7, 2, 3, BR)
        pyxel.rect(bx+6,  by+7, 2, 3, BR)
        pyxel.rect(bx+10, by+7, 2, 3, BR)
 
 
def draw_roof(rx, ry, rw, seed=0):
    # Hausstil basierend auf Position bestimmen
    style = int(rx // 80) % 4
 
    wall_top = ry + 13
    wall_h   = SH - wall_top
 
    # ── Wandfarbe je nach Stil ────────────────────────────────
    wall_cols = [PE, NV, DG, PU]
    wall_col  = wall_cols[style]
 
    # Hauptwand
    pyxel.rect(rx, wall_top, rw, wall_h, wall_col)
 
    # Mauerwerk-Textur (horizontale Fugenlinien)
    dark = BK
    for row in range(int(wall_top) + 6, SH, 6):
        pyxel.line(rx, row, rx + rw - 1, row, dark)
    # Versetzte vertikale Fugen
    toggle = 0
    for row in range(int(wall_top), SH, 6):
        offset = 3 if toggle % 2 == 0 else 0
        for col in range(int(rx) + offset, int(rx + rw), 6):
            pyxel.pset(col, row, dark)
        toggle += 1
 
    # ── Fenster ───────────────────────────────────────────────
    win_cols = [LB, YL, LG, OR]
    win_col  = win_cols[style]
    rows_of_wins = max(1, wall_h // 14)
    cols_of_wins = max(1, (rw - 6) // 12)
    for wr in range(rows_of_wins):
        wy = wall_top + 5 + wr * 13
        if wy + 7 > SH - 2:
            break
        for wc in range(cols_of_wins):
            wx = rx + 4 + wc * 12
            if wx + 6 > rx + rw - 2:
                break
            # Fensterrahmen
            pyxel.rect(wx,     wy,     7, 7, dark)
            # Fensterglas (hell = an, dunkel = aus)
            lit = ((int(rx) + wr * 3 + wc * 7) % 5) != 0
            glass = win_col if lit else DG
            pyxel.rect(wx + 1, wy + 1, 5, 5, glass)
            # Kreuz im Fenster
            pyxel.pset(wx + 3, wy + 1, dark)
            pyxel.pset(wx + 3, wy + 2, dark)
            pyxel.pset(wx + 3, wy + 3, dark)
            pyxel.pset(wx + 3, wy + 4, dark)
            pyxel.pset(wx + 3, wy + 5, dark)
            pyxel.line(wx + 1, wy + 3, wx + 5, wy + 3, dark)
 
    # ── Tür (nur wenn Haus breit genug) ───────────────────────
    if rw >= 24:
        dx = rx + rw // 2 - 4
        dy = SH - 12
        if dy > wall_top + 2:
            pyxel.rect(dx,     dy,     9, 12, dark)
            pyxel.rect(dx + 1, dy + 1, 7, 11, BR)
            # Türknauf
            pyxel.pset(dx + 6, dy + 6, YL)
            # Türbogen
            pyxel.line(dx, dy, dx + 4, dy - 3, dark)
            pyxel.line(dx + 8, dy, dx + 4, dy - 3, dark)
            pyxel.pset(dx + 4, dy - 3, dark)
 
    # ── Dachgesims ────────────────────────────────────────────
    pyxel.rect(rx - 3, ry + 8, rw + 6, 5, BR)
    pyxel.line(rx - 3, ry + 8, rx + rw + 2, ry + 8, YL)
 
    # ── Dach (Stil-abhängig) ──────────────────────────────────
    if style == 0:
        # Klassisch – rote Ziegel mit Rillen
        pyxel.rect(rx, ry, rw, 9, RD)
        for tx in range(int(rx) + 4, int(rx + rw), 8):
            pyxel.line(tx, ry, tx, ry + 9, BR)
        pyxel.line(rx, ry + 4, rx + rw, ry + 4, BR)
        pyxel.rect(rx + 2, ry - 2, rw - 4, 3, YL)
 
    elif style == 1:
        # Pagoda – grünes Spitzdach
        pyxel.rect(rx, ry, rw, 9, DG)
        for i in range(0, rw, 6):
            pyxel.line(rx + i, ry + 9, rx + i + 3, ry, LG)
        pyxel.rect(rx + 2, ry - 3, rw - 4, 4, GN)
        # Dachspitze
        pyxel.tri(rx + rw//2 - 4, ry - 3, rx + rw//2, ry - 9, rx + rw//2 + 4, ry - 3, YL)
        pyxel.pset(rx + rw//2, ry - 9, OR)
 
    elif style == 2:
        # Modern – flaches dunkelgraues Dach mit Rand
        pyxel.rect(rx - 2, ry, rw + 4, 6, BK)
        pyxel.rect(rx, ry + 1, rw, 4, NV)
        pyxel.line(rx - 2, ry, rx + rw + 1, ry, LB)
        # Aufbauten / Lüftungsschächte
        for ax in range(int(rx) + 8, int(rx + rw) - 8, 15):
            pyxel.rect(ax, ry - 5, 5, 5, DG)
            pyxel.line(ax + 2, ry - 5, ax + 2, ry - 8, NV)
            pyxel.rect(ax + 1, ry - 9, 3, 2, DG)
 
    else:
        # Tempel – purpurnes geschwungenes Dach
        pyxel.rect(rx, ry, rw, 9, PU)
        for tx in range(int(rx) + 3, int(rx + rw), 6):
            pyxel.line(tx, ry, tx, ry + 9, NV)
        # Geschwungene Dachenden
        pyxel.line(rx - 3, ry + 9, rx, ry + 3, OR)
        pyxel.line(rx + rw + 2, ry + 9, rx + rw - 1, ry + 3, OR)
        pyxel.rect(rx + rw//2 - 2, ry - 4, 5, 5, YL)
        pyxel.pset(rx + rw//2, ry - 4, OR)
 
 
# ================================================================
# HINTERGRUND
# ================================================================
 
def draw_sky(scroll):
    bands = [
        (0,   12, BK),
        (12,  28, NV),
        (28,  50, PU),
        (50,  72, DG),
        (72,  92, NV),
        (92, 112, DG),
    ]
    for y0, y1, col in bands:
        pyxel.rect(0, y0, SW, y1-y0, col)
    mx, my = 205, 30
    for r, col in [(24, NV), (21, PU), (18, YL)]:
        pyxel.circ(mx, my, r, col)
    pyxel.circ(mx+12, my-9, 15, NV)
    pyxel.circ(mx-3, my+5, 2, OR)
    pyxel.circ(mx+4, my-2, 1, OR)
 
 
def draw_stars(scroll, frame):
    random.seed(42)
    for i in range(80):
        sx = random.randint(0, 2000)
        sy = random.randint(2, 68)
        star_x = (sx - int(scroll * 0.012)) % SW
        phase = (frame // 20 + i * 7) % 6
        if phase < 5:
            pyxel.pset(star_x, sy, WH)
        else:
            pyxel.pset(star_x, sy, YL)
        if i % 15 == 0:
            pyxel.pset(star_x-1, sy, NV)
            pyxel.pset(star_x+1, sy, NV)
            pyxel.pset(star_x, sy-1, NV)
            pyxel.pset(star_x, sy+1, NV)
            pyxel.pset(star_x, sy, WH)
 
 
def draw_clouds(scroll, frame):
    random.seed(13)
    for i in range(8):
        base_x = random.randint(0, 2800)
        cy = random.randint(22, 58)
        cw = random.randint(28, 55)
        cx = (base_x - int(scroll * 0.06) - int(frame * 0.08)) % (SW + 80) - 40
        pyxel.elli(cx + cw//2 + 1, cy + 2, cw - 2, 8, NV)
        pyxel.elli(cx + cw//2,     cy,     cw,     9, 7)
        pyxel.elli(cx + cw//3,     cy - 3, cw//2,  7, 7)
        pyxel.elli(cx + 2*cw//3,   cy - 4, cw//2,  7, 7)
        pyxel.line(cx + cw//3 + 1, cy - 6, cx + 2*cw//3 - 1, cy - 6, WH)
 
 
def draw_mountains(scroll):
    ms = int(scroll * 0.035)
    random.seed(11)
    for i in range(9):
        bx = (i * 105 - ms) % (SW + 160) - 80
        bh = 38 + (i * 13) % 22
        bw = 85 + (i * 11) % 35
        pyxel.tri(bx, 114, bx + bw//2, 114-bh, bx + bw, 114, NV)
 
    ms = int(scroll * 0.07)
    random.seed(22)
    for i in range(10):
        bx = (i * 88 - ms) % (SW + 140) - 70
        bh = 30 + (i * 11) % 20
        bw = 68 + (i * 9) % 28
        pyxel.tri(bx, 114, bx + bw//2, 114-bh, bx + bw, 114, PU)
        px = bx + bw//2
        py = 114 - bh
        pyxel.tri(px-5, py+8, px, py, px+5, py+8, WH)
        pyxel.tri(px-3, py+5, px, py, px+3, py+5, LB)
 
    ms = int(scroll * 0.13)
    random.seed(33)
    for i in range(12):
        bx = (i * 72 - ms) % (SW + 110) - 55
        bh = 22 + (i * 9) % 16
        bw = 58 + (i * 7) % 22
        pyxel.tri(bx, 114, bx + bw//2, 114-bh, bx + bw, 114, DG)
        px = bx + bw//2
        py = 114 - bh
        pyxel.tri(px-4, py+6, px, py, px+4, py+6, WH)
 
    ms = int(scroll * 0.20)
    random.seed(44)
    for i in range(14):
        bx = (i * 58 - ms) % (SW + 90) - 45
        bh = 14 + (i * 7) % 13
        bw = 48 + (i * 5) % 18
        pyxel.tri(bx, 114, bx + bw//2, 114-bh, bx + bw, 114, NV)
 
 
def draw_city(scroll):
    cs = int(scroll * 0.22)
    random.seed(99)
    for i in range(20):
        bx = (i * 44 - cs) % (SW + 110) - 55
        bh = 20 + (i * 7) % 30
        bw = 11 + (i * 3) % 10
        wall_col = NV if i % 3 == 0 else (PU if i % 3 == 1 else DG)
        pyxel.rect(bx, 112 - bh, bw, bh, wall_col)
        for wy in range(112 - bh + 2, 112 - 2, 5):
            for wx in range(bx + 2, bx + bw - 1, 4):
                if (wx * 3 + wy + i * 7) % 5 != 0:
                    pyxel.rect(wx, wy, 2, 3, YL)
                else:
                    pyxel.rect(wx, wy, 2, 3, DG)
        if i % 5 == 0 and bh > 24:
            tx = bx + bw // 2 - 3
            ty = 112 - bh - 7
            pyxel.rect(tx + 1, ty + 4, 1, 3, BR)
            pyxel.rect(tx, ty, 6, 5, BR)
            pyxel.rect(tx - 1, ty + 4, 8, 1, BR)
        if i % 4 == 2 and bh > 18:
            ax = bx + bw // 2
            ay = 112 - bh
            pyxel.line(ax, ay, ax, ay - 5, LG)
            pyxel.pset(ax, ay - 5, RD)
    pyxel.rect(0, 112, SW, 2, BK)
 
 
def draw_lanterns(rooftops, scroll, frame):
    random.seed(55)
    for idx, r in enumerate(rooftops):
        if idx % 3 != 0:
            continue
        lx = r['x'] + r['w'] // 2
        ly = r['y'] - 1
        pyxel.line(lx, ly, lx, ly - 10, BR)
        swing = int(math.sin(frame * 0.04 + idx * 1.1) * 2)
        lx2 = lx + swing
        ly2 = ly - 10
        pyxel.rect(lx2 - 2, ly2 - 5, 5, 6, RD)
        pyxel.rect(lx2 - 1, ly2 - 6, 3, 1, YL)
        pyxel.rect(lx2 - 1, ly2 + 1, 3, 1, YL)
        if frame % 4 < 3:
            pyxel.pset(lx2, ly2 - 2, OR)
 
 
# ================================================================
# GAME ENGINE
# ================================================================
 
class Game:
    def __init__(self):
        pyxel.init(SW, SH, title="Kung Fu Run v7", fps=60)
        pyxel.load("res.pyxres")
        self.highscore = 0
        self._reset()
        pyxel.run(self.update, self.draw)
 
    def _reset(self):
        self.state  = S_PLAY
        self.frame  = 0
        self.scroll = 0
        self.score  = 0
        self.energy = 100.0
        self.px, self.py   = 30, 80
        self.pvx, self.pvy = 0, 0
        self.facing    = 1
        self.on_ground = False
        self.shurikens = []
        self.particles = []
        self.dog_freed = False
 
        # Meter-Tracking
        self.dist_pixels    = 0   # gelaufene Pixel (fuer Score)
        self.dist_score_acc = 0   # Akkumulator fuer Meter-Punkte
        self.roofs_crossed  = set()  # Indizes der passierten Daecher
 
        # Level-Generierung – Ziel bei GOAL_DISTANCE
        self.rooftops = []
        x, y = 0, 110
        # Erstelle genug Daecher um bis zum Ziel zu kommen
        while x < GOAL_DISTANCE + 200:
            w = random.randint(50, 110)
            self.rooftops.append({'x': x, 'y': y, 'w': w})
            x += w + random.randint(15, 35)
            y  = max(70, min(y + random.randint(-20, 20), 140))
 
        # Einheiten spawnen (nicht auf dem letzten Dach)
        self.ninjas = []
        self.traps  = []
        self.items  = []
        for idx, r in enumerate(self.rooftops[1:-3]):
            if random.random() < 0.4:
                self.ninjas.append({
                    'x': r['x'] + 20, 'y': r['y'] - 16,
                    'vx': 0.6, 'is_thrower': random.random() < 0.3,
                    'throw_cd': 0, 'alive': True, 'roof': r,
                    'roof_idx': idx + 1
                })
            if random.random() < 0.25:
                kind = 'spikes' if random.random() < 0.6 else 'cannon'
                self.traps.append({'x': r['x']+40, 'y': r['y']-8, 'kind': kind, 'cd': 0})
            if random.random() < 0.5:
                self.items.append({'x': r['x']+10, 'y': r['y']-15, 'kind': 'coin', 'collected': False})
 
        # Ziel-Plattform am Ende
        last = self.rooftops[-1]
        self.cage_x = last['x'] + 10
        self.cage_y = last['y'] - 38
 
    def update(self):
        # R = Neustart immer moeglich
        if pyxel.btnp(pyxel.KEY_R):
            self._reset()
            return
 
        if self.state != S_PLAY:
            return
 
        self.frame += 1
        self._upd_player()
        self._upd_ninjas()
        self._upd_shurikens()
        self._upd_traps()
        self._upd_items()
        self._upd_dist_score()
 
    def _upd_dist_score(self):
        """2 Punkte pro 10 gelaufene Pixel."""
        old_dist = self.dist_pixels
        new_dist = max(self.dist_pixels, int(self.px))
        if new_dist > old_dist:
            moved = new_dist - old_dist
            self.dist_score_acc += moved
            self.dist_pixels = new_dist
            while self.dist_score_acc >= 10:
                self.score += 2
                self.dist_score_acc -= 10
 
    def _upd_player(self):
        move  = False
        speed = BSPEED if pyxel.btn(pyxel.KEY_SHIFT) and self.energy > 5 else RSPEED
 
        if pyxel.btn(pyxel.KEY_A) or pyxel.btn(pyxel.KEY_LEFT):
            self.pvx = -speed; self.facing = -1; move = True
        elif pyxel.btn(pyxel.KEY_D) or pyxel.btn(pyxel.KEY_RIGHT):
            self.pvx = speed;  self.facing =  1; move = True
        else:
            self.pvx *= 0.7
 
        if (pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_UP)) and self.on_ground:
            self.pvy       = JFORCE
            self.on_ground = False
 
        self.pvy  += GRAV
        self.px   += self.pvx
        prev_py    = self.py
        self.py   += self.pvy
        self.energy = max(0, min(100, self.energy + (
            -0.5 if move and speed == BSPEED else 0.2)))
 
        # Dachlande-Kollision
        self.on_ground = False
        for idx, r in enumerate(self.rooftops):
            if self.px + 8 > r['x'] and self.px < r['x'] + r['w']:
                roof_top = r['y']
                if (prev_py + 16 <= roof_top + 1) and \
                   (self.py  + 16 >= roof_top) and \
                   self.pvy >= 0:
                    self.py        = roof_top - 16
                    self.pvy       = 0
                    self.on_ground = True
                    # Haus-Punkte: 10 Punkte fuer jedes neue Dach
                    if idx not in self.roofs_crossed and idx > 0:
                        self.roofs_crossed.add(idx)
                        self.score += 10
 
        if self.py > SH:
            self._end_game(False)
            return
 
        self.scroll = max(self.scroll, self.px - 60)
 
        # Hund befreien (nah am Kaefig + SPACE)
        if not self.dog_freed:
            if abs(self.px - self.cage_x) < 35 and abs(self.py - self.cage_y) < 35:
                if pyxel.btnp(pyxel.KEY_SPACE):
                    self.dog_freed = True
                    self.score    += 200
                    self._end_game(True)
 
    def _end_game(self, won):
        self.state = S_WIN if won else S_LOSE
        if self.score > self.highscore:
            self.highscore = self.score
 
    def _upd_ninjas(self):
        for n in self.ninjas:
            if not n['alive']:
                continue
            n['x'] += n['vx']
            if n['x'] < n['roof']['x'] or n['x'] > n['roof']['x'] + n['roof']['w'] - 10:
                n['vx'] *= -1
 
            dist = abs(n['x'] - self.px)
            if n['is_thrower'] and dist < THROW_RANGE and n['throw_cd'] <= 0:
                dir_ = 1 if self.px > n['x'] else -1
                self.shurikens.append({'x': n['x']+4, 'y': n['y']+4, 'vx': dir_ * SHURIKEN_SPEED})
                n['throw_cd'] = SHURIKEN_COOLDOWN
            if n['throw_cd'] > 0:
                n['throw_cd'] -= 1
 
            if abs(self.px - n['x']) < 10 and abs(self.py - n['y']) < 12:
                if self.pvy > 0 and self.py < n['y']:
                    n['alive']  = False
                    self.pvy    = STOMP_BOUNCE
                    self.score += 40   # 40 Punkte pro Ninja
                else:
                    self._end_game(False)
 
    def _upd_shurikens(self):
        for s in self.shurikens[:]:
            s['x'] += s['vx']
            if abs(s['x'] - self.px) < 6 and abs(s['y'] - (self.py+8)) < 8:
                self._end_game(False)
            if abs(s['x'] - self.px) > 300:
                self.shurikens.remove(s)
 
    def _upd_traps(self):
        for t in self.traps:
            if t['kind'] == 'cannon':
                t['cd'] -= 1
                if t['cd'] <= 0:
                    self.shurikens.append({'x': t['x'], 'y': t['y']+4, 'vx': -2.0})
                    t['cd'] = 150
            if abs(self.px - t['x']) < 8 and abs(self.py - t['y']) < 10:
                self._end_game(False)
 
    def _upd_items(self):
        for i in self.items:
            if not i['collected'] and abs(self.px - i['x']) < 12 and abs(self.py - i['y']) < 12:
                i['collected'] = True
                self.score    += 20   # 20 Punkte pro Coin
 
    def draw(self):
        pyxel.cls(0)
 
        draw_sky(self.scroll)
        draw_stars(self.scroll, self.frame)
        draw_clouds(self.scroll, self.frame)
        draw_mountains(self.scroll)
        draw_city(self.scroll)
 
        pyxel.camera(self.scroll, 0)
 
        for r in self.rooftops:
            draw_roof(r['x'], r['y'], r['w'])
 
        draw_lanterns(self.rooftops, self.scroll, self.frame)
 
        for i in self.items:
            if not i['collected']:
                draw_coin(i['x'], i['y'], 0, self.frame)
 
        for t in self.traps:
            if t['kind'] == 'spikes': draw_spikes(t['x'], t['y'])
            else:                     draw_cannon(t['x'], t['y'], -1)
 
        for n in self.ninjas:
            if n['alive']:
                draw_ninja(n['x'], n['y'], self.frame, 1, is_thrower=n['is_thrower'])
 
        for s in self.shurikens:
            draw_shuriken(s['x'], s['y'], self.frame)
 
        draw_dog_cage(self.cage_x, self.cage_y, self.dog_freed, self.frame)
 
        draw_player(self.px, self.py, self.frame, self.facing,
                    self.energy > 5 and pyxel.btn(pyxel.KEY_SHIFT))
 
        # ── HUD (keine Kamera) ────────────────────────────────────
        pyxel.camera()
 
        # Score (oben links)
        pyxel.rect(3, 3, 70, 20, BK)
        pyxel.rectb(3, 3, 70, 20, DG)
        pyxel.text(5, 5,  "SCORE",       DG)
        pyxel.text(5, 13, f"{self.score}", YL)
 
        # Highscore (oben links, unter Score)
        pyxel.rect(3, 25, 70, 11, BK)
        pyxel.rectb(3, 25, 70, 11, NV)
        pyxel.text(5, 27, f"BEST: {self.highscore}", LB)
 
        # Energie-Balken
        pyxel.rectb(4, 38, 52, 5, DG)
        pyxel.rect(5, 39, int(self.energy / 2), 3, LN)
 
        # Distanz-Anzeige (oben rechts)
        dist_m   = int(self.dist_pixels / 10)
        remaining = max(0, GOAL_DISTANCE // 10 - dist_m)
        pyxel.rect(SW - 90, 3, 87, 20, BK)
        pyxel.rectb(SW - 90, 3, 87, 20, DG)
        pyxel.text(SW - 88, 5,  f"Dist: {dist_m}m",     LG)
        if remaining > 0:
            pyxel.text(SW - 88, 13, f"Ziel: -{remaining}m", OR)
        else:
            pyxel.text(SW - 88, 13, "Ziel in Sicht!",       YL)
 
        # Punktetafel (rechts, als kleine Legende)
        pyxel.rect(SW - 60, 26, 57, 44, BK)
        pyxel.rectb(SW - 60, 26, 57, 44, NV)
        pyxel.text(SW - 58, 28, "Ninja  +40",  RD)
        pyxel.text(SW - 58, 36, "Haus   +10",  OR)
        pyxel.text(SW - 58, 44, "Meter  +2",   LG)
        pyxel.text(SW - 58, 52, "Coin   +20",  YL)
 
        # Win/Lose Screen
        if self.state == S_WIN:
            pyxel.rect(54, 68, 148, 56, BK)
            pyxel.rectb(54, 68, 148, 56, YL)
            pyxel.text(74, 74,  "MISSION ERFUELLT!", GN)
            pyxel.text(74, 84,  f"Score:     {self.score}", YL)
            if self.score >= self.highscore:
                pyxel.text(74, 94,  "NEUER HIGHSCORE!", OR)
            else:
                pyxel.text(74, 94,  f"Highscore: {self.highscore}", LB)
            pyxel.text(74, 104, "R = Neustart",    WH)
 
        if self.state == S_LOSE:
            pyxel.rect(54, 68, 148, 56, BK)
            pyxel.rectb(54, 68, 148, 56, RD)
            pyxel.text(74, 74,  "NINJAS SIEGTEN...", RD)
            pyxel.text(74, 84,  f"Score:     {self.score}", YL)
            if self.score >= self.highscore:
                pyxel.text(74, 94,  "NEUER HIGHSCORE!", OR)
            else:
                pyxel.text(74, 94,  f"Highscore: {self.highscore}", LB)
            pyxel.text(74, 104, "R = Neustart",    WH)
 
 
Game()