import pyxel
import random
import math

MAP_SIZE = 2048
SCREEN_W = 512
SCREEN_H = 512
HALF_W   = SCREEN_W // 2
HALF_H   = SCREEN_H // 2

# ── Farben ────────────────────────────────────────────────────────
COL_GRASS    = 3
COL_SIDEWALK = 4
COL_ASPHALT  = 5
COL_ASPH2    = 6
COL_WHITE    = 7
COL_RED      = 8
COL_ORANGE   = 9
COL_YELLOW   = 10
COL_GREEN    = 11
COL_BLUE     = 12
COL_LGRAY    = 13
COL_PINK     = 14

SIDEWALK_W = 6

# ── Strassennetz ─────────────────────────────────────────────────
ROADS_H = [
    {"y":  187, "x0":    0, "x1": 2048, "w": 20, "lanes": 4, "rough": 2},
    {"y":  541, "x0":    0, "x1": 2048, "w": 18, "lanes": 4, "rough": 2},
    {"y":  863, "x0":    0, "x1": 2048, "w": 16, "lanes": 3, "rough": 2},
    {"y": 1197, "x0":    0, "x1": 2048, "w": 20, "lanes": 4, "rough": 2},
    {"y": 1589, "x0":    0, "x1": 2048, "w": 18, "lanes": 4, "rough": 2},
    {"y": 1881, "x0":    0, "x1": 2048, "w": 14, "lanes": 3, "rough": 2},
    {"y":   83, "x0":   90, "x1":  780, "w": 12, "lanes": 2, "rough": 1},
    {"y":   83, "x0":  900, "x1": 1620, "w": 12, "lanes": 2, "rough": 1},
    {"y":  323, "x0":  300, "x1":  950, "w": 13, "lanes": 2, "rough": 1},
    {"y":  323, "x0": 1100, "x1": 1700, "w": 12, "lanes": 2, "rough": 1},
    {"y":  323, "x0": 1800, "x1": 2048, "w": 12, "lanes": 2, "rough": 1},
    {"y":  430, "x0":    0, "x1":  410, "w": 12, "lanes": 2, "rough": 1},
    {"y":  430, "x0":  530, "x1":  980, "w": 13, "lanes": 2, "rough": 1},
    {"y":  697, "x0":  150, "x1":  660, "w": 12, "lanes": 2, "rough": 1},
    {"y":  697, "x0":  820, "x1": 1380, "w": 12, "lanes": 2, "rough": 1},
    {"y":  697, "x0": 1500, "x1": 1980, "w": 13, "lanes": 2, "rough": 1},
    {"y":  779, "x0":    0, "x1":  350, "w": 12, "lanes": 2, "rough": 1},
    {"y":  979, "x0":  400, "x1":  890, "w": 12, "lanes": 2, "rough": 1},
    {"y":  979, "x0": 1050, "x1": 1600, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1063, "x0":    0, "x1":  560, "w": 13, "lanes": 2, "rough": 1},
    {"y": 1063, "x0":  700, "x1": 1200, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1063, "x0": 1700, "x1": 2048, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1341, "x0":  100, "x1":  730, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1341, "x0":  870, "x1": 1490, "w": 13, "lanes": 2, "rough": 1},
    {"y": 1341, "x0": 1600, "x1": 2048, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1450, "x0":    0, "x1":  480, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1450, "x0":  600, "x1": 1100, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1720, "x0":  200, "x1":  760, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1720, "x0":  900, "x1": 1440, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1720, "x0": 1580, "x1": 1990, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1800, "x0":    0, "x1":  380, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1970, "x0":  500, "x1": 1100, "w": 12, "lanes": 2, "rough": 1},
    {"y": 1970, "x0": 1250, "x1": 1850, "w": 12, "lanes": 2, "rough": 1},
]
ROADS_V = [
    {"x":  143, "y0":    0, "y1": 2048, "w": 16, "lanes": 3, "rough": 2},
    {"x":  479, "y0":    0, "y1": 2048, "w": 20, "lanes": 4, "rough": 2},
    {"x":  821, "y0":    0, "y1": 2048, "w": 18, "lanes": 4, "rough": 2},
    {"x": 1213, "y0":    0, "y1": 2048, "w": 20, "lanes": 4, "rough": 2},
    {"x": 1597, "y0":    0, "y1": 2048, "w": 18, "lanes": 4, "rough": 2},
    {"x": 1931, "y0":    0, "y1": 2048, "w": 14, "lanes": 3, "rough": 2},
    {"x":   47, "y0":   83, "y1":  541, "w": 12, "lanes": 2, "rough": 1},
    {"x":   47, "y0":  700, "y1": 1197, "w": 12, "lanes": 2, "rough": 1},
    {"x":   47, "y0": 1400, "y1": 1881, "w": 12, "lanes": 2, "rough": 1},
    {"x":  289, "y0":    0, "y1":  430, "w": 12, "lanes": 2, "rough": 1},
    {"x":  289, "y0":  580, "y1": 1063, "w": 13, "lanes": 2, "rough": 1},
    {"x":  289, "y0": 1197, "y1": 1720, "w": 12, "lanes": 2, "rough": 1},
    {"x":  289, "y0": 1850, "y1": 2048, "w": 12, "lanes": 2, "rough": 1},
    {"x":  631, "y0":  187, "y1":  697, "w": 12, "lanes": 2, "rough": 1},
    {"x":  631, "y0":  863, "y1": 1341, "w": 13, "lanes": 2, "rough": 1},
    {"x":  631, "y0": 1500, "y1": 1970, "w": 12, "lanes": 2, "rough": 1},
    {"x":  983, "y0":    0, "y1":  541, "w": 13, "lanes": 2, "rough": 1},
    {"x":  983, "y0":  700, "y1": 1197, "w": 12, "lanes": 2, "rough": 1},
    {"x":  983, "y0": 1341, "y1": 1800, "w": 12, "lanes": 2, "rough": 1},
    {"x":  983, "y0": 1920, "y1": 2048, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1071, "y0":  541, "y1":  863, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1071, "y0": 1197, "y1": 1450, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1399, "y0":   83, "y1":  541, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1399, "y0":  697, "y1": 1197, "w": 13, "lanes": 2, "rough": 1},
    {"x": 1399, "y0": 1341, "y1": 1881, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1751, "y0":  323, "y1":  863, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1751, "y0": 1063, "y1": 1589, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1751, "y0": 1720, "y1": 2048, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1853, "y0":    0, "y1":  323, "w": 12, "lanes": 2, "rough": 1},
    {"x": 1853, "y0":  863, "y1": 1341, "w": 12, "lanes": 2, "rough": 1},
    {"x": 2011, "y0":  541, "y1": 1197, "w": 12, "lanes": 2, "rough": 1},
    {"x": 2011, "y0": 1450, "y1": 1970, "w": 12, "lanes": 2, "rough": 1},
]

# ── Asphalt-Textur ────────────────────────────────────────────────
ASPHALT_PATCHES_H = {}
ASPHALT_PATCHES_V = {}

def _precompute_patches():
    for i, r in enumerate(ROADS_H):
        rng2 = random.Random(i * 9999)
        span = r["x1"] - r["x0"]
        pts = [(r["x0"] + rng2.randint(0, span-1),
                r["y"] - r["w"] + rng2.randint(0, r["w"]*2-1))
               for _ in range(span // 5)]
        ASPHALT_PATCHES_H[i] = pts
    for i, r in enumerate(ROADS_V):
        rng2 = random.Random((i+50) * 9999)
        span = r["y1"] - r["y0"]
        pts = [(r["x"] - r["w"] + rng2.randint(0, r["w"]*2-1),
                r["y0"] + rng2.randint(0, span-1))
               for _ in range(span // 5)]
        ASPHALT_PATCHES_V[i] = pts

_precompute_patches()

# ── Kollision ─────────────────────────────────────────────────────
def point_on_road(wx, wy):
    for r in ROADS_H:
        if r["x0"] - 2 <= wx <= r["x1"] + 2 and abs(wy - r["y"]) <= r["w"] + 1:
            return True
    for r in ROADS_V:
        if r["y0"] - 2 <= wy <= r["y1"] + 2 and abs(wx - r["x"]) <= r["w"] + 1:
            return True
    return False

def move_police(px, py, tx, ty, speed):
    dx, dy = tx - px, ty - py
    dist = math.hypot(dx, dy)
    if dist == 0: return px, py
    mx, my = (dx/dist)*speed, (dy/dist)*speed
    nx, ny = px + mx, py + my
    if point_on_road(nx, ny): return nx, ny
    if point_on_road(px + mx, py): return px + mx, py
    if point_on_road(px, py + my): return px, py + my
    return px + mx * 0.3, py + my * 0.3

# ── Block-Grenzen ─────────────────────────────────────────────────
def _road_h_boundaries():
    intervals = []
    for r in ROADS_H:
        intervals.append((r["y"] - r["w"] - SIDEWALK_W - 2, r["y"] + r["w"] + SIDEWALK_W + 2))
    intervals.sort()

    merged = []
    for current in intervals:
        if not merged:
            merged.append(current)
        else:
            prev = merged[-1]
            if current[0] <= prev[1]:
                merged[-1] = (prev[0], max(prev[1], current[1]))
            else:
                merged.append(current)

    gaps = []
    last_y = 0
    for m in merged:
        if m[0] > last_y:
            gaps.append((last_y, m[0]))
        last_y = m[1]
    if last_y < MAP_SIZE:
        gaps.append((last_y, MAP_SIZE))

    return [(g[0], g[1]) for g in gaps if g[1] - g[0] > 6]

def _road_v_boundaries():
    intervals = []
    for r in ROADS_V:
        intervals.append((r["x"] - r["w"] - SIDEWALK_W - 2, r["x"] + r["w"] + SIDEWALK_W + 2))
    intervals.sort()

    merged = []
    for current in intervals:
        if not merged:
            merged.append(current)
        else:
            prev = merged[-1]
            if current[0] <= prev[1]:
                merged[-1] = (prev[0], max(prev[1], current[1]))
            else:
                merged.append(current)

    gaps = []
    last_x = 0
    for m in merged:
        if m[0] > last_x:
            gaps.append((last_x, m[0]))
        last_x = m[1]
    if last_x < MAP_SIZE:
        gaps.append((last_x, MAP_SIZE))

    return [(g[0], g[1]) for g in gaps if g[1] - g[0] > 6]

H_STRIPS = _road_h_boundaries()
V_STRIPS = _road_v_boundaries()

# ── Gebäude – einmalig generiert, FEST ───────────────────────────
BUILDINGS    = []
WINDOW_CACHE = {}

def _fill_block(bx0, by0, bx1, by1, rng, block_id):
    if bx1-bx0 < 8 or by1-by0 < 8: return
    dist_c = math.hypot((bx0+bx1)/2-1024, (by0+by1)/2-1024)
    tower_p = max(0.05, 0.55-dist_c/1800)
    x, idx = bx0, 0
    while x < bx1:
        rem = bx1 - x
        if rem <= 0: break
        col_w = rng.randint(min(10, rem), min(32, rem))
        y = by0
        while y < by1:
            is_t = rng.random() < tower_p
            bh = max(8, min(rng.randint(20,48) if is_t else rng.randint(10,28), by1-y))
            bw = col_w
            col = rng.choice([1,2,12,5,9]) if is_t else rng.choice([1,2,4,8,9,14,2,1])
            wk = (block_id, idx)
            if wk not in WINDOW_CACHE:
                wr = random.Random(block_id*10000+idx)
                pat = {}
                if is_t:
                    for wy2 in range(4, bh-2, 5):
                        for wx2 in range(1, bw-1, 4):
                            lit = wr.random() > 0.2
                            pat[(wx2, wy2)] = lit
                            if bw >= 6: pat[(wx2+1, wy2)] = lit
                elif bw >= 10 and bh >= 10:
                    for wy2 in range(3, bh-2, 7):
                        for wx2 in range(2, bw-2, 6):
                            pat[(wx2, wy2)] = wr.random() > 0.4
                WINDOW_CACHE[wk] = pat
            BUILDINGS.append((x, y, bw, bh, col, is_t, wk))
            y += bh; idx += 1
        x += col_w

def _gen_blocks():
    rng = random.Random(42)
    bid = 0
    for (vy0, vy1) in H_STRIPS:
        for (vx0, vx1) in V_STRIPS:
            _fill_block(vx0, vy0, vx1, vy1, rng, bid); bid += 1

_gen_blocks()

# ── Partikel-System ───────────────────────────────────────────────
class Particle:
    __slots__ = ("x","y","vx","vy","life","max_life","col","size")
    def __init__(self, x, y, vx, vy, life, col, size=1):
        self.x, self.y   = float(x), float(y)
        self.vx, self.vy = vx, vy
        self.life        = life
        self.max_life    = life
        self.col         = col
        self.size        = size

PARTICLES = []

def _spawn_smoke(wx, wy, count=2):
    for _ in range(count):
        angle = random.uniform(0, math.pi * 2)
        speed = random.uniform(0.1, 0.5)
        PARTICLES.append(Particle(wx, wy, math.cos(angle)*speed, math.sin(angle)*speed - 0.4, random.randint(18,35), COL_LGRAY, 2))

def _spawn_fire(wx, wy, count=3):
    for _ in range(count):
        angle = random.uniform(0, math.pi * 2)
        speed = random.uniform(0.2, 0.8)
        PARTICLES.append(Particle(wx, wy, math.cos(angle)*speed, math.sin(angle)*speed - 0.6, random.randint(8,20), random.choice([COL_RED,COL_ORANGE,COL_YELLOW]), 1))

def _spawn_sparks(wx, wy, count=6):
    for _ in range(count):
        angle = random.uniform(0, math.pi * 2)
        speed = random.uniform(0.5, 2.5)
        PARTICLES.append(Particle(wx, wy, math.cos(angle)*speed, math.sin(angle)*speed, random.randint(5,12), random.choice([COL_YELLOW,COL_ORANGE,COL_WHITE]), 1))

def _spawn_debris(wx, wy, count=8):
    for _ in range(count):
        angle = random.uniform(0, math.pi * 2)
        speed = random.uniform(1.0, 4.0)
        PARTICLES.append(Particle(wx, wy, math.cos(angle)*speed, math.sin(angle)*speed, random.randint(15,30), random.choice([COL_LGRAY,1,5]), 2))

def _update_particles():
    dead = []
    for p in PARTICLES:
        p.x += p.vx; p.y += p.vy
        p.vy += 0.02; p.vx *= 0.97; p.vy *= 0.97
        p.life -= 1
        if p.life <= 0: dead.append(p)
    for p in dead: PARTICLES.remove(p)

# ── Spawn-Helfer: nur auf festen Strassen ─────────────────────────
def _spawn_bill_at_random(rng=None):
    r = rng or random
    for _ in range(200):
        if r.random() < 0.5:
            road = r.choice(ROADS_H)
            x = float(r.randint(road["x0"], road["x1"]))
            y = float(road["y"] + r.randint(-road["w"]//2, road["w"]//2))
        else:
            road = r.choice(ROADS_V)
            x = float(road["x"] + r.randint(-road["w"]//2, road["w"]//2))
            y = float(r.randint(road["y0"], road["y1"]))
        if point_on_road(x, y):
            return {"x": x, "y": y, "value": r.choice([10,20,50,100])}
    return None

BILL_MAX      = 20
BILL_RESPAWN  = 180
BILL_PICKUP_R = 12

# ══════════════════════════════════════════════════════════════════
class Game:
    def __init__(self):
        pyxel.init(SCREEN_W, SCREEN_H, title="Open City", fps=60)
        try: pyxel.load("res.pyxres")
        except Exception: pass
        pyxel.sounds[60].set("a0","p","1","n",10)
        pyxel.sounds[61].set("a1","p","2","n",10)
        pyxel.sounds[62].set("c1","n","6","f",15)
        self.money = 0; self.highscore = 0
        self.upg_speed = 0; self.upg_power = 0; self.upg_hp = 0
        self.state = "START"
        self._build()
        pyxel.run(self.update, self.draw)

    def _build(self):
        PARTICLES.clear()
        self.score = 0; self.wanted_level = 1
        self.police_timer = 0; self.penalty_timer = 0
        self.game_over = False; self.busted = False
        self.px = 479.0; self.py = 541.0
        self.heading = 0.0; self.speed = 0.0; self.cam_angle = 0.0
        self.player_in_car = False; self.current_car = None
        self.is_walking = False; self.facing_dir = 1
        self.max_crashes = 3 + self.upg_hp
        self.car_health_max = self.max_crashes * 34
        self.car_health = self.car_health_max
        self.offroad_timer = 0; self.car_state = "ok"
        self.dead_timer = 0; self.shake = 0
        self.bills = []; self.bill_timer = 0
        self.powerups = []; self.powerup_timer = 0
        self.boost_timer = 0; self.double_money_timer = 0
        self._init_bills()
        
        self.pedestrians = []; self.police = []; self.cars = []
        # Ein Auto direkt am Spawn platzieren
        self.cars.append({"x": 495.0, "y": 541.0, "col": COL_BLUE}) 
        self._spawn_cars()

    def _get_upgrade_cost(self, level):
        return 999999 if level >= 5 else int(100 * (2 ** level))

    def _init_bills(self):
        rng = random.Random(999)
        for _ in range(10):
            b = _spawn_bill_at_random(rng)
            if b: self.bills.append(b)

    def _spawn_on_road(self, rng=None):
        r = rng or random
        for _ in range(200):
            if r.random() < 0.5:
                road = r.choice(ROADS_H)
                x = float(r.randint(road["x0"], road["x1"]))
                y = float(road["y"] + r.randint(-road["w"]//2, road["w"]//2))
            else:
                road = r.choice(ROADS_V)
                x = float(road["x"] + r.randint(-road["w"]//2, road["w"]//2))
                y = float(r.randint(road["y0"], road["y1"]))
            if point_on_road(x, y): return x, y
        return 479.0, 541.0

    def _spawn_cars(self):
        rng = random.Random(7)
        cols = [COL_RED, COL_ORANGE, COL_PINK, 2, 15]
        for i in range(18):
            x, y = self._spawn_on_road(rng)
            self.cars.append({"x": x, "y": y, "col": cols[i % len(cols)]})

    def _spawn_pedestrian(self):
        if len(self.pedestrians) >= 35: return
        x, y = self._spawn_on_road()
        self.pedestrians.append({"x": x, "y": y})

    def _spawn_police(self):
        for _ in range(50):
            angle = random.uniform(0, 2*math.pi)
            dist  = random.uniform(200, 350)
            x = self.px + math.cos(angle)*dist
            y = self.py + math.sin(angle)*dist
            if 0 <= x <= MAP_SIZE and 0 <= y <= MAP_SIZE and point_on_road(x, y):
                self.police.append({"x": x, "y": y}); return

    # ── Kamera ────────────────────────────────────────────────────
    def _w2s(self, wx, wy):
        dx, dy = wx - self.px, wy - self.py
        rad = math.radians(-self.cam_angle)
        cos_a, sin_a = math.cos(rad), math.sin(rad)
        rx = dx*cos_a - dy*sin_a
        ry = dx*sin_a + dy*cos_a
        sx = random.randint(-self.shake, self.shake) if self.shake > 0 else 0
        sy = random.randint(-self.shake, self.shake) if self.shake > 0 else 0
        return int(HALF_W + rx + sx), int(HALF_H + ry + sy)

    # ── Sichtbarkeitscheck im WELTKOORDINATEN-Raum ────────────────
    def _world_vis(self, wx, wy, margin=500):
        # Margin von 500 garantiert, dass beim Drehen keine Objekte in den Bildschirmecken despawnen
        return abs(wx - self.px) < margin and abs(wy - self.py) < margin

    def _vis(self, sx, sy, m=80):
        return -m <= sx <= SCREEN_W+m and -m <= sy <= SCREEN_H+m

    # ── Update ────────────────────────────────────────────────────
    def update(self):
        if self.state == "START":
            if pyxel.btnp(pyxel.KEY_RETURN): self.state = "PLAY"; self._build()
            elif pyxel.btnp(pyxel.KEY_U): self.state = "SHOP"
            return
        if self.state == "SHOP":
            if pyxel.btnp(pyxel.KEY_RETURN): self.state = "START"
            c = self._get_upgrade_cost(self.upg_speed)
            if pyxel.btnp(pyxel.KEY_1) and self.money >= c and self.upg_speed < 5:
                self.money -= c; self.upg_speed += 1
            c = self._get_upgrade_cost(self.upg_power)
            if pyxel.btnp(pyxel.KEY_2) and self.money >= c and self.upg_power < 5:
                self.money -= c; self.upg_power += 1
            c = self._get_upgrade_cost(self.upg_hp)
            if pyxel.btnp(pyxel.KEY_3) and self.money >= c and self.upg_hp < 5:
                self.money -= c; self.upg_hp += 1
            return
        if self.game_over:
            _update_particles()
            if self.car_state == "dead" and pyxel.frame_count % 3 == 0:
                _spawn_fire(self.px, self.py, 2); _spawn_smoke(self.px, self.py, 2)
            if pyxel.btnp(pyxel.KEY_R): self.state = "START"
            return

        _update_particles()
        if self.shake > 0: self.shake -= 1
        if self.penalty_timer > 0: self.penalty_timer -= 1
        if self.boost_timer > 0: self.boost_timer -= 1
        if self.double_money_timer > 0: self.double_money_timer -= 1

        self.score += 1
        if self.score > self.highscore: self.highscore = self.score
        if self.score > 6000:   self.wanted_level = 5
        elif self.score > 3500: self.wanted_level = 4
        elif self.score > 1500: self.wanted_level = 3
        elif self.score > 500:  self.wanted_level = 2
        else:                   self.wanted_level = 1

        self.police_timer += 1
        if self.police_timer >= max(30, 150 - self.wanted_level*20):
            self.police_timer = 0
            if len(self.police) < self.wanted_level*2: self._spawn_police()

        self.bill_timer += 1
        if self.bill_timer >= BILL_RESPAWN:
            self.bill_timer = 0
            for _ in range(3 if len(self.bills) < BILL_MAX//2 else 1):
                if len(self.bills) < BILL_MAX:
                    b = _spawn_bill_at_random()
                    if b: self.bills.append(b)

        for bill in self.bills[:]:
            if math.hypot(self.px-bill["x"], self.py-bill["y"]) < BILL_PICKUP_R:
                val = bill["value"] * (2 if self.double_money_timer > 0 else 1)
                self.money += val
                _spawn_sparks(bill["x"], bill["y"], 4)
                self.bills.remove(bill)

        if self.upg_power > 0:
            self.powerup_timer += 1
            if self.powerup_timer >= 700-self.upg_power*100 and len(self.powerups) < 4:
                self.powerup_timer = 0
                p = _spawn_bill_at_random()
                if p: p["type"] = random.choice(["SPEED","MONEY"]); self.powerups.append(p)
        for p in self.powerups[:]:
            if math.hypot(self.px-p["x"], self.py-p["y"]) < BILL_PICKUP_R:
                if p["type"] == "SPEED": self.boost_timer = 900
                else: self.double_money_timer = 900
                _spawn_sparks(p["x"], p["y"], 6); self.powerups.remove(p)

        if self.player_in_car: self._update_car()
        else: self._update_foot()

        if self.player_in_car:
            self.cam_angle = (self.cam_angle + ((self.heading-self.cam_angle+540)%360-180)*0.2) % 360
        else:
            self.cam_angle += ((0-self.cam_angle+540)%360-180)*0.10

        if pyxel.frame_count % 55 == 0: self._spawn_pedestrian()

        if pyxel.btnp(pyxel.KEY_E):
            if self.player_in_car:
                if self.current_car: self.current_car["x"], self.current_car["y"] = self.px, self.py
                self.player_in_car = False; self.current_car = None; self.speed = 0.0; self.cam_angle = 0.0
            else:
                for car in self.cars:
                    if math.hypot(self.px-car["x"], self.py-car["y"]) < 18:
                        self.player_in_car = True; self.current_car = car
                        self.car_health = self.car_health_max; self.car_state = "ok"; self.offroad_timer = 0
                        break

        if self.player_in_car and abs(self.speed) > 0.5:
            for ped in self.pedestrians[:]:
                if math.hypot(self.px-ped["x"], self.py-ped["y"]) < 18:
                    self.score = max(0, self.score-500); self.penalty_timer = 60
                    _spawn_sparks(ped["x"], ped["y"], 5); self.pedestrians.remove(ped)

        pol_speed = 2.5 + self.wanted_level*0.4
        for p in self.police[:]:
            p["x"], p["y"] = move_police(p["x"], p["y"], self.px, self.py, pol_speed)
            if math.hypot(self.px-p["x"], self.py-p["y"]) < 12:
                if not self.player_in_car or abs(self.speed) < 1.0:
                    self.busted = True; self.game_over = True; self.speed = 0.0; break

    def _update_foot(self):
        dx, dy = 0.0, 0.0
        if pyxel.btn(pyxel.KEY_W): dy -= 1
        if pyxel.btn(pyxel.KEY_S): dy += 1
        if pyxel.btn(pyxel.KEY_A): dx -= 1
        if pyxel.btn(pyxel.KEY_D): dx += 1
        if dx > 0: self.facing_dir = 1
        elif dx < 0: self.facing_dir = -1
        if dx == 0 and dy == 0: self.is_walking = False; return
        self.is_walking = True
        mag = math.hypot(dx, dy)
        self.heading = math.degrees(math.atan2(dx, -dy))
        # Laufgeschwindigkeit halbiert (von 3.0 auf 1.5)
        nx = max(0, min(MAP_SIZE, self.px + dx/mag*1.5))
        ny = max(0, min(MAP_SIZE, self.py + dy/mag*1.5))
        if point_on_road(nx, self.py): self.px = nx
        if point_on_road(self.px, ny): self.py = ny

    # Alle Auto-Werte gedrosselt
    CAR_ACCEL=0.15; CAR_BRAKE=0.25; CAR_MAX_FWD=4.5
    CAR_MAX_REV=2.0; CAR_FRICTION=0.9; STEER_RATE=4.0

    def _update_car(self):
        gas=pyxel.btn(pyxel.KEY_W); brake=pyxel.btn(pyxel.KEY_S)
        left=pyxel.btn(pyxel.KEY_A); right=pyxel.btn(pyxel.KEY_D)
        if pyxel.frame_count % 8 == 0:
            pyxel.play(0, 61 if gas and self.speed > 0.5 else 60)
        mult = 1.0 + self.upg_speed*0.05
        if self.boost_timer > 0: mult *= 1.5
        eff_max = self.CAR_MAX_FWD*mult; eff_acc = self.CAR_ACCEL*mult
        if gas: self.speed = min(self.speed+eff_acc, eff_max)
        elif brake:
            self.speed = max(0.0, self.speed-self.CAR_BRAKE) if self.speed > 0.1 else max(-self.CAR_MAX_REV, self.speed-eff_acc*0.7)
        else:
            self.speed *= self.CAR_FRICTION
            if abs(self.speed) < 0.05: self.speed = 0.0
        sf = max(min(abs(self.speed)/eff_max, 1.0), 0.5)
        sd = self.STEER_RATE * sf * (-1 if self.speed < -0.1 else 1)
        if left:  self.heading = (self.heading-sd) % 360
        if right: self.heading = (self.heading+sd) % 360
        if abs(self.speed) > 0.01:
            rad = math.radians(self.heading)
            mx = math.sin(rad)*self.speed; my = -math.cos(rad)*self.speed
            nx = max(0, min(MAP_SIZE, self.px+mx))
            ny = max(0, min(MAP_SIZE, self.py+my))
            on_nx = point_on_road(nx, self.py)
            on_ny = point_on_road(self.px, ny)
            cx = not on_nx and abs(mx) > 1.0
            cy = not on_ny and abs(my) > 1.0
            if (cx or cy) and abs(self.speed) > 1.5:
                imp = abs(self.speed); self.speed *= -0.3
                self._take_damage(20); self.shake = max(self.shake, int(imp*2))
                _spawn_sparks(self.px, self.py, int(imp*2)); pyxel.play(1,62)
            else:
                if on_nx: self.px = nx
                if on_ny: self.py = ny
            for other in self.cars+self.police:
                if other is self.current_car: continue
                d = math.hypot(self.px-other["x"], self.py-other["y"])
                if d < 14:
                    if abs(self.speed) > 1.5:
                        imp = abs(self.speed); self.speed *= -0.5
                        self._take_damage(int(imp*5)); self.shake = max(self.shake, int(imp*2))
                        _spawn_sparks(self.px, self.py, int(imp*3)); pyxel.play(1,62)
                    a = math.atan2(self.py-other["y"], self.px-other["x"])
                    self.px += math.cos(a)*2; self.py += math.sin(a)*2
            if not point_on_road(self.px, self.py):
                self.offroad_timer += 1
                if self.offroad_timer > 10 and self.offroad_timer % 6 == 0: self._take_damage(2)
            else: self.offroad_timer = max(0, self.offroad_timer-2)
        if self.current_car: self.current_car["x"], self.current_car["y"] = self.px, self.py
        hp = self.car_health/self.car_health_max
        if hp < 0.3:
            self.car_state = "fire"
            if pyxel.frame_count % 2 == 0: _spawn_fire(self.px,self.py,3); _spawn_smoke(self.px,self.py,2)
        elif hp < 0.6:
            self.car_state = "smoke"
            if pyxel.frame_count % 5 == 0: _spawn_smoke(self.px,self.py,2)
        else: self.car_state = "ok"
        if self.car_health <= 0: self._trigger_explosion()

    def _take_damage(self, a): self.car_health = max(0, self.car_health-a)

    def _trigger_explosion(self):
        self.game_over=True; self.car_state="dead"; self.dead_timer=60; self.speed=0.0; self.shake=8
        _spawn_debris(self.px,self.py,12); _spawn_fire(self.px,self.py,10); _spawn_smoke(self.px,self.py,8)
        pyxel.play(1,62); self.wanted_level = min(self.wanted_level+2,5)

    # ── Zeichnen ──────────────────────────────────────────────────
    def draw(self):
        if self.state == "START":
            pyxel.cls(0)
            pyxel.text(SCREEN_W//2-20, 80,  "OPEN CITY", COL_WHITE)
            pyxel.text(SCREEN_W//2-38, 110, f"HIGHSCORE: {self.highscore}", COL_ORANGE)
            pyxel.text(SCREEN_W//2-38, 160, "Press ENTER to Play", COL_YELLOW)
            pyxel.text(SCREEN_W//2-32, 180, "Press U for Shop", COL_GREEN)
            pyxel.rectb(SCREEN_W//2-50, 145, 100, 50, COL_LGRAY)
            return
        if self.state == "SHOP":
            pyxel.cls(0)
            pyxel.text(20,20,"--- UPGRADE SHOP ---",COL_YELLOW)
            pyxel.text(20,40,f"Available Money: ${self.money}",COL_GREEN)
            c1=f"${self._get_upgrade_cost(self.upg_speed)}" if self.upg_speed<5 else "MAX"
            c2=f"${self._get_upgrade_cost(self.upg_power)}" if self.upg_power<5 else "MAX"
            c3=f"${self._get_upgrade_cost(self.upg_hp)}" if self.upg_hp<5 else "MAX"
            pyxel.text(20,70,f"[1] Speed (+5%): Lvl {self.upg_speed}/5 - {c1}",COL_WHITE)
            pyxel.text(20,90,f"[2] Power-Ups:   Lvl {self.upg_power}/5 - {c2}",COL_WHITE)
            pyxel.text(20,110,f"[3] Car HP (+1): Lvl {self.upg_hp}/5 - {c3}",COL_WHITE)
            pyxel.text(20,180,"Press ENTER to return to Menu",COL_LGRAY)
            return

        pyxel.cls(COL_GRASS)
        # Reihenfolge: Gebäude zuerst, dann Strassen drüber
        self._draw_buildings()
        self._draw_roads()
        self._draw_bills()
        self._draw_powerups()
        self._draw_pedestrians()
        self._draw_other_cars()
        self._draw_police_units()
        self._draw_player()
        self._draw_particles()
        self._draw_hud()
        if self.game_over:
            pyxel.rect(0, HALF_H-25, SCREEN_W, 50, 0)
            text = "BUSTED" if self.busted else "WASTED"
            tc = COL_RED if not self.busted or (pyxel.frame_count//5)%2==0 else COL_BLUE
            pyxel.text(HALF_W-len(text)*2, HALF_H-10, text, tc)
            pyxel.text(HALF_W-len("Press R to Menu")*2, HALF_H+10, "Press R to Menu", COL_WHITE)

    def _draw_buildings(self):
        for (bx, by, bw, bh, col, is_t, wk) in BUILDINGS:
            cx = bx + bw//2; cy = by + bh//2
            if not self._world_vis(cx, cy, 500): continue
            c0=self._w2s(bx,by); c1=self._w2s(bx+bw,by)
            c2=self._w2s(bx+bw,by+bh); c3=self._w2s(bx,by+bh)
            pyxel.tri(c0[0],c0[1],c1[0],c1[1],c2[0],c2[1],col)
            pyxel.tri(c0[0],c0[1],c2[0],c2[1],c3[0],c3[1],col)
            pyxel.line(c0[0],c0[1],c1[0],c1[1],min(col+2,15))
            for (wx2,wy2),lit in WINDOW_CACHE.get(wk,{}).items():
                fx,fy=self._w2s(bx+wx2,by+wy2)
                if 0<=fx<SCREEN_W and 0<=fy<SCREEN_H:
                    pyxel.pset(fx,fy,7 if lit else 1)

    def _draw_roads(self):
        for i, r in enumerate(ROADS_H):
            ry,w,lanes,x0,x1,sw = r["y"],r["w"],r["lanes"],r["x0"],r["x1"],SIDEWALK_W
            # Verbesserte Prüfung: Ist der Spieler auf der horizontalen Achse irgendwo auf der Strasse?
            if not (x0 - 500 <= self.px <= x1 + 500 and abs(ry - self.py) <= 500): continue
            
            self._fill_wrect(x0,ry-w-sw,x1-x0,sw,COL_SIDEWALK)
            self._fill_wrect(x0,ry+w,x1-x0,sw,COL_SIDEWALK)
            self._fill_wrect(x0,ry-w,x1-x0,w*2,COL_ASPHALT)
            for (px2,py2) in ASPHALT_PATCHES_H.get(i,[]):
                if not self._world_vis(px2,py2,500): continue
                sx2,sy2=self._w2s(px2,py2)
                if 0<=sx2<SCREEN_W and 0<=sy2<SCREEN_H: pyxel.pset(sx2,sy2,COL_ASPH2)
            for lane in range(1,lanes):
                ly=ry-w+(w*2*lane//lanes)
                dc=COL_YELLOW if lanes>=3 else COL_WHITE
                seg=int(x0//18)*18
                while seg<x1:
                    s,e=max(seg,x0),min(seg+10,x1)
                    if e>s and self._world_vis((s+e)//2,ly,500):
                        ax,ay=self._w2s(s,ly); bx2,by2=self._w2s(e,ly)
                        if self._vis(ax,ay,8): pyxel.line(ax,ay,bx2,by2,dc)
                    seg+=18
        for i, r in enumerate(ROADS_V):
            rx,w,lanes,y0,y1,sw = r["x"],r["w"],r["lanes"],r["y0"],r["y1"],SIDEWALK_W
            # Verbesserte Prüfung für vertikale Strassen
            if not (y0 - 500 <= self.py <= y1 + 500 and abs(rx - self.px) <= 500): continue
            
            self._fill_wrect(rx-w-sw,y0,sw,y1-y0,COL_SIDEWALK)
            self._fill_wrect(rx+w,y0,sw,y1-y0,COL_SIDEWALK)
            self._fill_wrect(rx-w,y0,w*2,y1-y0,COL_ASPHALT)
            for (px2,py2) in ASPHALT_PATCHES_V.get(i,[]):
                if not self._world_vis(px2,py2,500): continue
                sx2,sy2=self._w2s(px2,py2)
                if 0<=sx2<SCREEN_W and 0<=sy2<SCREEN_H: pyxel.pset(sx2,sy2,COL_ASPH2)
            for lane in range(1,lanes):
                lx=rx-w+(w*2*lane//lanes)
                dc=COL_YELLOW if lanes>=3 else COL_WHITE
                seg=int(y0//18)*18
                while seg<y1:
                    s,e=max(seg,y0),min(seg+10,y1)
                    if e>s and self._world_vis(lx,(s+e)//2,500):
                        ax,ay=self._w2s(lx,s); bx2,by2=self._w2s(lx,e)
                        if self._vis(ax,ay,8): pyxel.line(ax,ay,bx2,by2,dc)
                    seg+=18
        for rh in ROADS_H:
            for rv in ROADS_V:
                if rh["x0"]<=rv["x"]<=rh["x1"] and rv["y0"]<=rh["y"]<=rv["y1"]:
                    if abs(rv["x"] - self.px) < 500 and abs(rh["y"] - self.py) < 500:
                        self._fill_wrect(rv["x"]-rv["w"],rh["y"]-rh["w"],rv["w"]*2,rh["w"]*2,COL_ASPHALT)

    def _draw_bills(self):
        for bill in self.bills:
            if not self._world_vis(bill["x"],bill["y"],500): continue
            sx,sy=self._w2s(bill["x"],bill["y"])
            if not self._vis(sx,sy,8): continue
            pyxel.rect(sx-4,sy-2,8,5,COL_GREEN); pyxel.rect(sx-3,sy-1,6,3,3); pyxel.pset(sx,sy,COL_GREEN)
            if bill["value"]>=50 and (pyxel.frame_count//10)%2==0:
                pyxel.pset(sx-5,sy-3,COL_YELLOW); pyxel.pset(sx+4,sy-3,COL_YELLOW)

    def _draw_powerups(self):
        for p in self.powerups:
            if not self._world_vis(p["x"],p["y"],500): continue
            sx,sy=self._w2s(p["x"],p["y"])
            if not self._vis(sx,sy,8): continue
            col=COL_BLUE if p["type"]=="SPEED" else COL_YELLOW
            pyxel.circ(sx,sy,4,col); pyxel.circb(sx,sy,4,COL_WHITE)
            pyxel.text(sx-1,sy-2,"S" if p["type"]=="SPEED" else "D",0)

    def _draw_pedestrians(self):
        for ped in self.pedestrians:
            sx,sy=self._w2s(ped["x"],ped["y"])
            if self._vis(sx,sy,4): pyxel.circ(sx,sy,2,COL_YELLOW); pyxel.pset(sx,sy,0)

    def _draw_other_cars(self):
        for car in self.cars:
            if car is not self.current_car: self._draw_car_sprite(car["x"],car["y"],car["col"])

    def _draw_police_units(self):
        for p in self.police:
            self._draw_car_sprite(p["x"],p["y"],COL_BLUE)
            sx,sy=self._w2s(p["x"],p["y"])
            if self._vis(sx,sy,8):
                blink=(pyxel.frame_count//6)%2
                pyxel.pset(sx-1,sy-5,7 if blink else COL_BLUE)
                pyxel.pset(sx+1,sy-5,COL_BLUE if blink else 7)

    def _draw_player(self):
        cx,cy=HALF_W,HALF_H
        if self.car_state=="dead":
            pyxel.rect(cx-5,cy-8,10,16,COL_LGRAY); pyxel.rect(cx-4,cy-7,8,14,1)
            pyxel.line(cx-5,cy-8,cx+4,cy+7,COL_LGRAY); pyxel.line(cx+4,cy-8,cx-5,cy+7,COL_LGRAY)
            return
        if self.player_in_car:
            col=COL_ORANGE if self.car_state=="fire" else COL_YELLOW if self.car_state=="smoke" else COL_GREEN
            pyxel.rect(cx-4,cy-8,8,16,col); pyxel.rect(cx-3,cy-7,6,4,COL_ASPH2)
            pyxel.rect(cx-3,cy+3,6,3,COL_ASPH2)
            pyxel.rect(cx-6,cy-7,2,4,0); pyxel.rect(cx+4,cy-7,2,4,0)
            pyxel.rect(cx-6,cy+3,2,4,0); pyxel.rect(cx+4,cy+3,2,4,0)
            pyxel.rect(cx-1,cy-5,2,10,min(col+2,15))
            if pyxel.btn(pyxel.KEY_S) and self.speed>0:
                pyxel.pset(cx-3,cy+6,COL_RED); pyxel.pset(cx+2,cy+6,COL_RED)
        else:
            pyxel.blt(cx-8,cy-16,0,0,32,16*self.facing_dir,32,7)

    def _draw_car_sprite(self, wx, wy, col):
        sx,sy=self._w2s(wx,wy)
        if not self._vis(sx,sy,14): return
        pyxel.rect(sx-4,sy-7,8,14,col); pyxel.rect(sx-3,sy-6,6,4,COL_ASPH2)
        pyxel.rect(sx-3,sy+2,6,3,COL_ASPH2)
        pyxel.rect(sx-6,sy-6,2,4,0); pyxel.rect(sx+4,sy-6,2,4,0)
        pyxel.rect(sx-6,sy+2,2,4,0); pyxel.rect(sx+4,sy+2,2,4,0)

    def _draw_particles(self):
        for p in PARTICLES:
            sx,sy=self._w2s(p.x,p.y)
            if self._vis(sx,sy,4):
                col=COL_ASPH2 if p.life/p.max_life<0.3 else p.col
                if p.size>=2: pyxel.rect(sx-1,sy-1,p.size,p.size,col)
                else: pyxel.pset(sx,sy,col)

    def _draw_hud(self):
        pyxel.rect(0,0,SCREEN_W,25,0)
        pyxel.text(4,5,f"$ {self.money}",COL_GREEN)
        pyxel.text(4,15,f"SCORE: {self.score}",COL_WHITE)
        if self.penalty_timer>0: pyxel.text(60,15,"-500!",COL_RED)
        wc=[COL_LGRAY,COL_YELLOW,COL_ORANGE,COL_RED,COL_RED,COL_RED][self.wanted_level]
        pyxel.text(SCREEN_W-self.wanted_level*5-4,5,"*"*self.wanted_level,wc)
        pyxel.text(HALF_W-14,5,"[E]Auto" if not self.player_in_car else "[E]raus",COL_LGRAY)
        if self.player_in_car:
            kmh=int(abs(self.speed)/(self.CAR_MAX_FWD*(1.0+self.upg_speed*0.05)*(1.5 if self.boost_timer>0 else 1.0))*120)
            pyxel.text(SCREEN_W//2+20,5,f"{kmh}km/h",COL_WHITE)
        if self.player_in_car or self.car_state=="dead":
            bx2,by2=SCREEN_W-64,SCREEN_H-12
            hw=int(60*self.car_health/self.car_health_max)
            pyxel.rect(bx2,by2,60,6,1)
            if hw>0:
                hc=COL_RED if self.car_health/self.car_health_max<0.3 else COL_ORANGE if self.car_health/self.car_health_max<0.6 else COL_GREEN
                pyxel.rect(bx2,by2,hw,6,hc)
            pyxel.text(bx2-45,by2,f"HP:{int(self.car_health//34)}",COL_WHITE)
        if self.boost_timer>0: pyxel.text(4,25,f"SPEED: {self.boost_timer//30}s",COL_BLUE)
        if self.double_money_timer>0: pyxel.text(4,35,f"$$$: {self.double_money_timer//30}s",COL_YELLOW)
        ar=math.radians(self.cam_angle)
        pyxel.circ(18,SCREEN_H-18,8,1)
        pyxel.line(18,SCREEN_H-18,int(18+math.sin(ar)*6),int(SCREEN_H-18-math.cos(ar)*6),COL_WHITE)
        pyxel.text(15,SCREEN_H-30,"N",COL_YELLOW)

    def _fill_wrect(self, wx, wy, ww, wh, col):
        c0=self._w2s(wx,wy); c1=self._w2s(wx+ww,wy)
        c2=self._w2s(wx+ww,wy+wh); c3=self._w2s(wx,wy+wh)
        pyxel.tri(c0[0],c0[1],c1[0],c1[1],c2[0],c2[1],col)
        pyxel.tri(c0[0],c0[1],c2[0],c2[1],c3[0],c3[1],col)

Game()