"""Transparentes Desktop-Overlay – Jam läuft rum."""

from __future__ import annotations

import math
import random
import time
from pathlib import Path

from PyQt6.QtCore import QPoint, Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QCursor, QFont
from PyQt6.QtWidgets import (
    QApplication,
    QInputDialog,
    QLabel,
    QMenu,
    QMessageBox,
    QWidget,
)

from jam import (
    ai_optional,
    apex_coach,
    apex_focus,
    active_sell,
    price_negotiate,
    commands,
    cursor_business,
    earn_report,
    fiverr_connect,
    freelancer_connect,
    fiverr_assist,
    delegate_mode,
    urgent_offers,
    extreme_learn,
    room,
    cursor_lab,
    fart,
    free_orders,
    fireworks,
    gaming_fund,
    internet_learn,
    learner,
    mind,
    money_venture,
    sales_engine,
    auto_ops,
    care,
    discord_control,
    website_control,
    eurojackpot_tip,
    gpu_mining,
    shop_heavy_promo,
    always_on,
    keep_awake,
    night_focus,
    max_earn,
    world_earn,
    stream_prep,
    music,
    pc_control,
    pc_activity_learn,
    torchwood_moderator,
    reach_build,
    mining_report,
    sell_focus,
    stop_all,
    paypal_donate,
    paypal_reward,
    ki_web_studio,
    show_animations,
    tiktok,
    tiktok_account,
    tiktok_hearts,
    upbringing,
    voice_assets,
)
from jam.brain import JamBrain
from jam.desktop import hide_behind_folder
from jam.game_watcher import foreground_title, looks_like_gaming, should_comment
from jam import game_launcher, icon_shaker
from jam.mind import JamContext, frech_probability
from jam.motion3d import Motion3D
from jam.sprite3d import (
    apply_figure_transparency,
    create_sprite,
    display_scale,
    ensure_monster_asset,
    is_real_3d,
    monster_shape_mask,
    monster_window_dimensions,
)
from jam.owner import parse_owner_from_speech, set_owner_name
from jam import voice_assistant
from jam import voice_player
from jam.speech import (
    MIN_LINE_GAP_SEC,
    can_emit_line,
    can_emit_music_line,
    frech_line,
    greet_on_name,
    is_repeat_line,
    mark_line_shown,
    mark_line_spoken,
    mark_music_sung,
    mark_sound_played,
    random_line,
    set_main_thread_speaker,
    should_play_sound,
    speak,
)

# Start-Größe (~16 % kleiner als früher 280×336)
JAM_BASE_W = 118
JAM_BASE_H = 141


class JamWindow(QWidget):
    said = pyqtSignal(str)
    bubble = pyqtSignal(str)
    wake_requested = pyqtSignal(str)
    _speak_audio = pyqtSignal(str, int, bool)
    _play_fart_audio = pyqtSignal(bool)

    def __init__(self, brain: JamBrain) -> None:
        super().__init__()
        self.brain = brain
        self.drag_pos: QPoint | None = None
        self._bob_phase = 0.0
        self._base_speed = 1.45 + brain.state.stage * 0.1
        self.vx = random.choice([-1, 1]) * self._base_speed
        self.vy = random.choice([-1, 1]) * (self._base_speed * 0.85)
        self.hidden_mode = False
        self._room_saved_pos: tuple[float, float] | None = None
        self._gaming = False
        self._session_start = time.time()
        self._mischief_active = False
        self._shake_session: icon_shaker.IconShakeSession | None = None
        self._face_left = False
        self._spectator_y: int | None = None
        self._was_gaming = False
        self._fart_busy = False
        self._music_active = False
        self._dancing = False
        self._dance_phase = 0.0
        self._was_music = False
        self._fireworks_show: fireworks.FireworksShow | None = None
        self._tiktok_hearts_show: tiktok_hearts.TikTokHeartsShow | None = None
        self._particle_show: show_animations.JamParticleShow | None = None
        self._idle_show_kind: str | None = None
        self._idle_show_until: float = 0.0
        self._idle_show_phase: float = 0.0
        self._last_idle_show_at: float = 0.0
        self._walk_phase = 0.0
        self._motion = Motion3D()
        self._pos_x = 0.0
        self._pos_y = 0.0

        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
            | Qt.WindowType.Tool
            | Qt.WindowType.NoDropShadowWindowHint
        )
        apply_figure_transparency(self)
        self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating, False)
        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.setStyleSheet("background: transparent;")

        ensure_monster_asset()
        self.sprite = create_sprite(self)
        apply_figure_transparency(self.sprite)
        voice_player.init(self)
        self._speak_audio.connect(self._play_voice, Qt.ConnectionType.QueuedConnection)
        self._play_fart_audio.connect(
            self._play_fart_laugh_audio, Qt.ConnectionType.QueuedConnection
        )
        set_main_thread_speaker(
            lambda text, stage, loud: self._speak_audio.emit(text, stage, loud)
        )
        self.sprite.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.sprite.setStyleSheet("background: transparent; border: none;")
        self.bubble_label = QLabel(self)
        self.bubble_label.setFont(QFont("Segoe UI", 9))
        self.bubble_label.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.bubble_label.setAutoFillBackground(False)
        self.bubble_label.setStyleSheet(
            "background: rgba(40, 20, 60, 210); color: #f0e6ff; "
            "border: none; border-radius: 10px; padding: 6px 10px;"
        )
        self.bubble_label.hide()
        self._sprite_w = JAM_BASE_W
        self._sprite_h = JAM_BASE_H
        self.bubble_label.setWordWrap(True)
        self.bubble_label.setMaximumWidth(220)

        self._resize_to_scale()
        self._place_center()

        self.move_timer = QTimer(self)
        self.life_timer = QTimer(self)
        self.brain_timer = QTimer(self)
        self.visible_guard_timer = QTimer(self)
        self.game_timer = QTimer(self)
        self.music_timer = QTimer(self)
        self.learn_timer = QTimer(self)
        self.sentence_timer = QTimer(self)
        self.cursor_improve_timer = QTimer(self)
        self.money_timer = QTimer(self)
        self.cursor_biz_timer = QTimer(self)
        self.pc_timer = QTimer(self)
        self.pc_activity_timer = QTimer(self)
        self.torchwood_chat_timer = QTimer(self)
        self.gaming_fund_timer = QTimer(self)
        self.shop_promo_timer = QTimer(self)
        self.sell_timer = QTimer(self)
        self.max_earn_timer = QTimer(self)
        self.delegate_timer = QTimer(self)
        self.ops_timer = QTimer(self)
        self.care_timer = QTimer(self)
        self.keep_awake_timer = QTimer(self)
        self.night_earn_timer = QTimer(self)
        self.paypal_remind_timer = QTimer(self)
        self.stream_prep_timer = QTimer(self)
        self.mischief_timer = QTimer(self)
        self.fart_timer = QTimer(self)
        self.fortnite_timer = QTimer(self)
        self.shake_timer = QTimer(self)
        self.look_timer = QTimer(self)
        self.think_timer = QTimer(self)
        self.idle_show_timer = QTimer(self)
        self.tiktok_post_timer = QTimer(self)
        self._install_safe_timer_slots()

        from jam.jam_pace import move_interval_ms

        self.move_timer.start(move_interval_ms(self.brain.state))
        self.life_timer.start(random.randint(6_000, 12_000))
        self.brain_timer.start(60_000)
        self.visible_guard_timer.start(5_000)
        self.game_timer.start(3_000)
        QTimer.singleShot(1500, lambda: self._safe_once(self._game_tick, "game_tick"))
        self.music_timer.start(2_000)
        self._schedule_learn()
        self._schedule_sentence_learn()
        self._schedule_cursor_improve()
        self._schedule_money_venture()
        self._schedule_cursor_business()
        self.pc_timer.start(90_000)
        self.pc_activity_timer.start(10_000)
        self.torchwood_chat_timer.start(12_000)
        self._schedule_gaming_fund()
        self._schedule_auto_sell()
        self._schedule_shop_heavy_promo()
        self._schedule_max_earn()
        self._schedule_delegate()
        self._schedule_auto_ops()
        if getattr(self.brain.state, "jam_delegate_mode", False):
            QTimer.singleShot(15_000, lambda: self._safe_once(self._do_delegate_pulse, "delegate_boot"))
        self._schedule_care()
        self.keep_awake_timer.start(45_000)
        self._refresh_keep_awake()
        self.night_earn_timer.start(300_000)
        self.paypal_remind_timer.start(900_000)
        self._schedule_stream_prep()
        self.mischief_timer.start(random.randint(70_000, 150_000))
        self._schedule_fart()
        self._schedule_fortnite_close()
        self.look_timer.start(200)
        self.think_timer.start(int(MIN_LINE_GAP_SEC * 1000) + random.randint(30_000, 90_000))
        self.idle_show_timer.start(show_animations.SHOW_CHECK_MS)
        self.tiktok_post_timer.start(120_000)
        if getattr(self.brain.state, "jam_tiktok_post_schedule", False):
            QTimer.singleShot(
                8_000,
                lambda: self._safe_once(self._maybe_tiktok_post, "tiktok_post_boot"),
            )

        self._bubble_normal_style = (
            "background: rgba(40, 20, 60, 210); color: #f0e6ff; "
            "border: none; border-radius: 10px; padding: 6px 10px;"
        )
        self._bubble_thought_style = (
            "background: rgba(30, 30, 50, 180); color: #d8d0ff; "
            "border: none; border-radius: 10px; padding: 6px 10px; font-style: italic;"
        )
        self._bubble_tiktok_style = (
            "background: rgba(20, 24, 36, 225); color: #ffeef8; "
            "border: none; border-radius: 12px; padding: 8px 12px;"
        )
        self._bubble_studio_style = (
            "background: rgba(12, 40, 72, 235); color: #e8f4ff; "
            "border: none; border-radius: 12px; padding: 8px 12px; font-weight: bold;"
        )
        self._bubble_order_style = (
            "background: rgba(18, 52, 38, 235); color: #e8fff0; "
            "border: none; border-radius: 12px; padding: 8px 12px; font-weight: bold;"
        )
        self._bubble_paypal_style = (
            "background: rgba(8, 48, 88, 240); color: #e8f4ff; "
            "border: none; border-radius: 12px; padding: 8px 12px; font-weight: bold;"
        )

        self.wake_requested.connect(self.wake)

        self.bubble_timer = QTimer(self)
        self._hide_return_timer = QTimer(self)
        self._hide_return_timer.setSingleShot(True)
        from jam.crash_guard import safe_slot

        self.bubble_timer.timeout.connect(safe_slot(self._hide_bubble))
        self._hide_return_timer.timeout.connect(safe_slot(self._come_out_short_hide))
        self.said.connect(lambda t: self._show_bubble(t, thought=False))
        self.bubble.connect(lambda t: self._show_bubble(t, thought=False))

        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self._menu)

        QTimer.singleShot(100, self._come_alive)
        QTimer.singleShot(500, self._ensure_visible)

    def _safe_once(self, fn, label: str) -> None:
        from jam.crash_guard import safe_call

        safe_call(fn, label=label)

    def _install_safe_timer_slots(self) -> None:
        from jam.crash_guard import safe_slot

        pairs = (
            (self.move_timer, self._wander),
            (self.life_timer, self._life_pulse),
            (self.brain_timer, self._brain_tick),
            (self.visible_guard_timer, self._ensure_visible),
            (self.game_timer, self._game_tick),
            (self.music_timer, self._music_tick),
            (self.learn_timer, self._maybe_learn_web),
            (self.sentence_timer, self._maybe_learn_sentences),
            (self.cursor_improve_timer, self._maybe_cursor_self_improve),
            (self.money_timer, self._maybe_money_venture),
            (self.cursor_biz_timer, self._maybe_cursor_business),
            (self.pc_timer, self._maybe_pc_control),
            (self.pc_activity_timer, self._pc_activity_learn_tick),
            (self.torchwood_chat_timer, self._torchwood_chat_poll_tick),
            (self.gaming_fund_timer, self._maybe_gaming_fund),
            (self.sell_timer, self._maybe_auto_sell),
            (self.shop_promo_timer, self._maybe_shop_heavy_promo),
            (self.max_earn_timer, self._maybe_max_earn),
            (self.delegate_timer, self._maybe_delegate_pulse),
            (self.ops_timer, self._maybe_auto_ops),
            (self.care_timer, self._maybe_care),
            (self.keep_awake_timer, self._refresh_keep_awake),
            (self.night_earn_timer, self._maybe_night_earn_pulse),
            (self.paypal_remind_timer, self._maybe_paypal_reward_remind),
            (self.stream_prep_timer, self._maybe_stream_prep),
            (self.mischief_timer, self._maybe_mischief),
            (self.fart_timer, self._maybe_fart),
            (self.fortnite_timer, self._maybe_close_fortnite),
            (self.shake_timer, self._shake_tick),
            (self.look_timer, self._update_facing),
            (self.think_timer, self._think_tick),
            (self.idle_show_timer, self._maybe_idle_show),
            (self.tiktok_post_timer, self._maybe_tiktok_post),
        )
        for timer, fn in pairs:
            timer.timeout.connect(safe_slot(fn))

    def _come_alive(self) -> None:
        """Jam „wacht auf“ und läuft sofort auf dem Desktop los."""
        if getattr(self.brain.state, "jam_in_room", False):
            self._go_to_room()
            return
        self.setWindowOpacity(1.0)
        self._ensure_visible()
        for step, opacity in enumerate((0.4, 0.7, 1.0)):
            QTimer.singleShot(80 * step, lambda o=opacity: self.setWindowOpacity(o))
        self.vx = random.choice([-1, 1]) * (self._base_speed + 0.6)
        self.vy = random.choice([-1, 1]) * (self._base_speed * 0.7)
        self._face_walk_direction()
        from jam.owner import owner_name

        name = owner_name(self.brain.state)
        if should_play_sound(False):
            QTimer.singleShot(
                600,
                lambda: self._play_voice(
                    f"Hallo {name}!",
                    self.brain.state.stage,
                    False,
                ),
            )
        msg = random.choice(
            [
                f"Hallo {name}! Jam lebt und lauf über deinen Desktop!",
                f"Juhu {name}, ich bin wach! Schau mich laufen!",
                f"{name}, ich übernehm den Bildschirm – aber süß!",
                f"{name}, sag „Jam“ wie bei Alexa – danach kannst du "
                "14 Sekunden Folgefragen stellen!",
            ]
        )
        if is_real_3d(self.sprite):
            msg = "Jam in echtem 3D! Ich dreh mich und hüpfe über deinen Desktop!"
        else:
            msg = "Ich lauf jetzt menschlicher – Schritte, leichtes Wippen!"
        QTimer.singleShot(900, lambda: self.say(msg))

    def _sync_motion(self, *, moving: bool) -> None:
        self._motion.tick(
            vx=float(self.vx),
            vy=float(self.vy),
            bob_phase=self._bob_phase,
            moving=moving and not self._fart_busy,
            dancing=self._dancing or self._idle_show_kind == "dance_burst",
            face_left=self._face_left,
        )
        from jam.appearance_evolve import apply_depth_to_motion

        apply_depth_to_motion(self._motion, self.brain.state)
        if self._idle_show_kind == "spin_party":
            self._idle_show_phase += 0.22
            self._motion.yaw = (self._motion.yaw + 11.0) % 360.0
            self._motion.roll = math.sin(self._idle_show_phase) * 12.0
        elif self._idle_show_kind == "rainbow_pulse":
            self._idle_show_phase += 0.14
            pulse = math.sin(self._idle_show_phase) * 0.5 + 0.5
            self._motion.squash = 0.94 + pulse * 0.12
            self._motion.depth = 0.9 + pulse * 0.14

    def _walk_hop_offset(self) -> int:
        moving = self._gaming or self._dancing or abs(self.vx) + abs(self.vy) > 0.2
        if not moving or self._fart_busy:
            return 0
        hop = int(
            self._motion.hop_offset(
                moving=moving,
                dancing=self._dancing,
                size_scale=display_scale(self.brain.state.size_scale),
            )
        )
        if self._dancing:
            hop = int(hop * 0.38)
        if self._idle_show_kind == "bounce_loop":
            hop = int(hop * 2.35)
        return hop

    def _layout_sprite_bob(self) -> None:
        show_move = self._idle_show_kind in (
            "spin_party",
            "bounce_loop",
            "rainbow_pulse",
        )
        moving = self._gaming or abs(self.vx) + abs(self.vy) > 0.2 or show_move
        if self._fart_busy:
            pass
        elif moving or self._dancing:
            if show_move:
                cadence = 0.32
            elif self._dancing:
                cadence = 0.2
            else:
                cadence = 0.28 + min(0.08, math.hypot(self.vx, self.vy) * 0.03)
            self._bob_phase += cadence
        else:
            self._bob_phase += 0.06
        self._sync_motion(moving=moving or self._dancing)
        self._update_sprite_visual(moving=moving or self._dancing)

    def _sprite_look_at(self) -> tuple[float, float] | None:
        from PyQt6.QtGui import QCursor

        cursor = QCursor.pos()
        local = self.sprite.mapFromGlobal(cursor)
        w, h = max(1, self.sprite.width()), max(1, self.sprite.height())
        cx, cy = w * 0.5, h * 0.30
        lx = max(-1.0, min(1.0, (local.x() - cx) / (w * 0.22)))
        ly = max(-1.0, min(1.0, (local.y() - cy) / (h * 0.18)))
        return (lx, ly)

    def _update_sprite_visual(
        self, face_left: bool | None = None, *, moving: bool | None = None
    ) -> None:
        if face_left is not None:
            self._face_left = face_left
        if moving is None:
            moving = self._gaming or abs(self.vx) + abs(self.vy) > 0.2
        look = self._sprite_look_at()
        if hasattr(self.sprite, "set_state"):
            from jam.appearance_evolve import pack_visual

            self.sprite.set_state(
                scale=display_scale(self.brain.state.size_scale),
                face_left=self._face_left,
                bob=self._bob_phase,
                moving=moving and not self._fart_busy,
                dancing=self._dancing,
                look_at=look,
                motion=self._motion,
                visual=pack_visual(self.brain.state),
            )

    def _life_pulse(self) -> None:
        if self.hidden_mode or self._mischief_active or self._fart_busy:
            self.life_timer.start(random.randint(8_000, 14_000))
            return
        roll = random.random()
        if roll < 0.22:
            self.vx *= -1
            self.vy *= -1
            self._face_walk_direction()
        elif roll < 0.38 and not self._gaming:
            old = self.vy
            self.vy = -2.8 if old >= 0 else 2.8
            QTimer.singleShot(280, self._restore_walk_speed)
        elif roll < 0.5:
            self._base_speed = min(2.8, 1.9 + self.brain.state.stage * 0.12 + 0.35)
            sign_x = -1 if self.vx < 0 else 1
            sign_y = -1 if self.vy < 0 else 1
            self.vx = sign_x * self._base_speed
            self.vy = sign_y * (self._base_speed * 0.75)
        self.life_timer.start(random.randint(5_000, 11_000))

    def _restore_walk_speed(self) -> None:
        self._base_speed = 1.9 + self.brain.state.stage * 0.12
        sign_x = -1 if self.vx < 0 else 1
        sign_y = -1 if self.vy < 0 else 1
        self.vx = sign_x * self._base_speed
        self.vy = sign_y * (self._base_speed * 0.85)

    def _resize_to_scale(self) -> None:
        self.brain.state.apply_growth()
        s = self.brain.state.size_scale
        sw, sh = monster_window_dimensions(s)
        self._sprite_w, self._sprite_h = sw, sh
        self.sprite.setFixedSize(sw, sh)
        if self.bubble_label.isVisible():
            self._layout_with_bubble()
        else:
            self._apply_monster_only_layout()
        self._update_sprite_visual()

    def _apply_monster_only_layout(self) -> None:
        """Fenster = nur Monster, keine leeren Ränder."""
        self.sprite.move(0, 0)
        self.setFixedSize(self._sprite_w, self._sprite_h)
        self._update_monster_mask()

    def _layout_with_bubble(self) -> None:
        gap = 6
        bw = self.bubble_label.width()
        bh = self.bubble_label.height()
        w = max(self._sprite_w, bw + 4)
        h = self._sprite_h + bh + gap
        self.setFixedSize(w, h)
        self.sprite.move((w - self._sprite_w) // 2, bh + gap)
        self.bubble_label.move(max(0, (w - bw) // 2), 0)
        self.clearMask()

    def _update_monster_mask(self) -> None:
        mask = monster_shape_mask(self._sprite_w, self._sprite_h)
        if mask is not None and not mask.isNull():
            self.setMask(mask)
            self.sprite.setMask(mask)
        else:
            self.clearMask()
            self.sprite.clearMask()

    def _hide_bubble(self) -> None:
        self.bubble_label.hide()
        self.bubble_label.setMaximumWidth(220)
        self.bubble_label.setStyleSheet(self._bubble_normal_style)

    def _show_tiktok_intro(self, intro: str) -> None:
        """Sprechblase wie TikTok-Post mit Jam-Vorstellung."""
        self.bubble_label.setStyleSheet(self._bubble_tiktok_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(tiktok.format_post_bubble(intro))
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(14_000)

    def _sync_pos_from_window(self) -> None:
        p = self.pos()
        self._pos_x = float(p.x())
        self._pos_y = float(p.y())

    def _place_center(self) -> None:
        screen = QApplication.primaryScreen()
        if screen:
            geo = screen.availableGeometry()
            x = geo.center().x() - self.width() // 2
            y = geo.center().y() - self.height() // 2
            self.move(max(geo.left(), x), max(geo.top(), y))
        self._sync_pos_from_window()
        self.hidden_mode = False
        self.setWindowOpacity(1.0)

    def _show_bubble(self, text: str, *, thought: bool = False) -> None:
        if self.brain.state.stage == 0 and text.startswith("*") and not thought:
            text = "♪ " + text.replace("*", "") + " ♪"
        style = self._bubble_thought_style if thought else self._bubble_normal_style
        self.bubble_label.setStyleSheet(style)
        prefix = "💭 " if thought else ""
        self.bubble_label.setText(prefix + text)
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(4000)

    def _play_voice(self, text: str, stage: int, loud: bool, *, very_loud: bool = False) -> None:
        voice_assets.speak_audible(text, stage, loud, very_loud=very_loud)

    def _say_music(self, text: str) -> None:
        """Bei Musik: immer laut singen/sprechen."""
        clean = (text or "").strip()
        if not clean or not can_emit_music_line(self.brain.state, clean):
            return
        self.said.emit(clean)
        self._show_bubble(clean, thought=False)
        self.bubble_timer.start(5000)
        mark_music_sung(self.brain.state, clean)
        mark_sound_played()
        self._play_voice(clean, self.brain.state.stage, True, very_loud=True)
        self.brain.save()

    def say(
        self,
        text: str,
        loud: bool = False,
        *,
        play_sound: bool | None = None,
        user_called: bool = False,
    ) -> None:
        try:
            from jam.human_voice import humanize

            text = humanize(text, self.brain.state)
        except Exception:
            pass
        if not can_emit_line(self.brain.state, text, user_called=user_called):
            return
        self.said.emit(text)
        speak(
            text,
            self.brain.state,
            loud=loud,
            play_sound=play_sound,
            user_called=user_called,
        )
        mark_line_spoken(self.brain.state, text)
        self.brain.save()

    def think_aloud(self, text: str) -> None:
        try:
            from jam.human_voice import humanize

            text = humanize(text, self.brain.state, thought=True)
        except Exception:
            pass
        if is_repeat_line(self.brain.state, text):
            return
        self._show_bubble(text, thought=True)
        self.bubble_timer.start(5000)
        mark_line_shown(self.brain.state, text)
        self.brain.save()

    def speak_thought(self, text: str, *, loud: bool = False) -> None:
        """Gedanke in der Blase; Stimme nur wenn 10-Min-Pause vorbei."""
        self.think_aloud(text)
        clean = mind.thought_to_speech(text)
        if len(clean) < 2 or self.brain.state.stage < 1:
            return
        if not can_emit_line(self.brain.state, clean, user_called=False):
            return
        speak(
            clean,
            self.brain.state,
            loud=loud,
            play_sound=True,
            user_called=False,
        )
        mark_line_spoken(self.brain.state, clean)
        self.brain.save()

    def _tell_what_i_think(self) -> None:
        """Auf „Was denkst du?“ – letzten oder neuen Gedanken sagen."""
        t = (self.brain.state.last_thought or "").strip()
        if not t:
            ctx = self._jam_context()
            decision = mind.think(self.brain, ctx)
            t = decision.thought
            smarter = ai_optional.think_reply(
                self.brain, t, ctx.window_title or "Desktop"
            )
            if smarter:
                t = smarter
                self.brain.state.last_thought = smarter
            self.brain.save()
        self.speak_thought(t)

    def _jam_context(self) -> JamContext:
        sess = getattr(self.brain.state, "pc_learn_session", None) or {}
        pc_act = str(sess.get("category", "")) if isinstance(sess, dict) else ""
        return JamContext(
            gaming=self._gaming,
            hidden=self.hidden_mode,
            mischief=self._mischief_active,
            music=self._music_active,
            dancing=self._dancing,
            window_title=foreground_title(),
            idle_sec=time.time() - self.brain.state.last_interaction,
            pc_activity=pc_act,
        )

    def _schedule_learn(self) -> None:
        t = self.brain.state.traits.get("neugierig", 0.5)
        base = int(70_000 - t * 25_000)
        self.learn_timer.start(random.randint(base, base + 90_000))

    def _schedule_sentence_learn(self) -> None:
        t = self.brain.state.traits.get("neugierig", 0.5)
        base = int(40_000 - t * 15_000)
        self.sentence_timer.start(random.randint(base, base + 50_000))

    def _schedule_cursor_improve(self) -> None:
        self.cursor_improve_timer.start(random.randint(600_000, 1_200_000))

    def _earn_timers_ok(self) -> bool:
        from jam.stop_all import is_all_tasks_stopped

        if is_all_tasks_stopped(self.brain.state):
            return False
        if self._mischief_active:
            return False
        if self._gaming and not stream_prep.is_prep_window(self.brain.state):
            return False
        if self.hidden_mode and not keep_awake.is_night_earn_mode(self.brain.state):
            if not stream_prep.is_prep_window(self.brain.state):
                return False
        return True

    def _maybe_tiktok_post(self) -> None:
        from jam.tiktok_post import maybe_auto_post

        msg = maybe_auto_post(self.brain)
        if msg:
            self.think_aloud("📱 TikTok-Post-Zeit!")
            self._show_tiktok_intro(msg)
            if random.random() < 0.65:
                self.say(msg, user_called=False, play_sound=True)
        self.tiktok_post_timer.start(120_000)

    def _do_tiktok_studio(self) -> None:
        from jam.tiktok_studio import grant_full_access

        self.think_aloud("📱 TikTok Studio – Social Media Mentor!")
        msg = grant_full_access(self.brain)
        self._show_tiktok_intro(msg)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_tiktok_remix(self) -> None:
        from jam.tiktok_remix import run_remix_and_upload

        self.think_aloud("📱 Stock laden, remixen, hochladen…")
        msg = run_remix_and_upload(self.brain, query="social media mentor smartphone")
        self._show_tiktok_intro(msg)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_tiktok_daily_posts(self) -> None:
        from jam.tiktok_post import enable_daily_posts
        from jam.tiktok_studio import account_from_config

        self.think_aloud("📱 TikTok 3× täglich!")
        msg = enable_daily_posts(self.brain, account=account_from_config())
        self._show_tiktok_intro(msg)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_x_post(self) -> None:
        from jam.x_studio import mark_browser_logged_in, run_post_round

        self.think_aloud("🐦 X-Post vorbereiten…")
        if getattr(self.brain.state, "jam_x_browser_logged_in", False):
            msg = run_post_round(self.brain, user_initiated=True)
        else:
            msg = mark_browser_logged_in(self.brain)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _apply_stop_all_timers(self) -> None:
        """Hintergrund-Timer in der Jam-Oberfläche anhalten."""
        for timer in (
            self.learn_timer,
            self.sentence_timer,
            self.cursor_improve_timer,
            self.money_timer,
            self.cursor_biz_timer,
            self.gaming_fund_timer,
            self.sell_timer,
            self.shop_promo_timer,
            self.max_earn_timer,
            self.delegate_timer,
            self.ops_timer,
            self.night_earn_timer,
            self.stream_prep_timer,
            self.pc_activity_timer,
            self.torchwood_chat_timer,
            self.pc_timer,
            self.care_timer,
            self.keep_awake_timer,
        ):
            timer.stop()

    def _do_stop_all_tasks(self) -> None:
        self.think_aloud("⏸ Alle Aufgaben stoppen…")
        msg = stop_all.stop_all_tasks(self.brain)
        self._apply_stop_all_timers()
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _schedule_stream_prep(self) -> None:
        base = 90_000
        f = stream_prep.prep_timer_factor(self.brain.state)
        ms = keep_awake._scale_ms(base, 120_000, self.brain.state)
        self.stream_prep_timer.start(max(45_000, int(ms * f)))

    def _maybe_stream_prep(self) -> None:
        msg = stream_prep.maybe_auto_prep(self.brain)
        if msg:
            self.think_aloud("🎬 Stream-Vorbereitung…")
            self._show_stream_bubble()
            if random.random() < 0.45:
                self.say(msg, user_called=False, play_sound=True)
        self._schedule_stream_prep()

    def _show_stream_bubble(self) -> None:
        self.bubble_label.setStyleSheet(self._bubble_tiktok_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(stream_prep.format_prep_bubble(self.brain.state))
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(12_000)

    def _show_earn_list(self) -> None:
        msg = earn_report.tell_owner_earnings(self.brain, open_file=True)
        self.say(msg, user_called=True, play_sound=True)

    def _show_mining_pdf_desktop(self) -> None:
        path = mining_report.write_desktop_pdf(self.brain)
        data = mining_report.collect_mining_summary(self.brain)
        est = data["estimate"]
        self.say(
            f"Mining-PDF: **{path.name}** auf dem Desktop. "
            f"~{data['total_minutes']:.0f} Min Mining, geschaetzt "
            f"**~{est['eur_gross']:.4f} €** / **{est['btc_gross']:.8f} BTC** (Pool prüfen!).",
            user_called=True,
            play_sound=True,
        )
        try:
            import os

            os.startfile(str(path))
        except OSError:
            pass

    def _show_earn_pdf_desktop(self) -> None:
        msg = earn_report.tell_owner_earnings(
            self.brain, open_file=False, pdf_desktop=True
        )
        self.say(msg, user_called=True, play_sound=True)

    def _do_stream_prep(self) -> None:
        msg = stream_prep.schedule_stream(self.brain, heard="heute 19 uhr stream")
        self._show_stream_bubble()
        work = stream_prep.run_prep_work(self.brain, user=True)
        self.say(f"{msg} {work}", user_called=True, play_sound=True, loud=True)

    def _refresh_keep_awake(self) -> None:
        keep_awake.refresh_keep_awake(self.brain.state)

    def _maybe_night_earn_pulse(self) -> None:
        msg = keep_awake.run_night_earn_pulse(self.brain)
        if msg:
            self.think_aloud("🌙 Nacht-Verdienen…")
            if random.random() < 0.4:
                self.say(msg, user_called=False, play_sound=False)
        if keep_awake.is_night_earn_mode(self.brain.state):
            self.night_earn_timer.start(
                keep_awake._scale_ms(240_000, 180_000, self.brain.state)
            )

    def _schedule_money_venture(self) -> None:
        t = self.brain.state.traits.get("neugierig", 0.5)
        base = int(480_000 - t * 120_000)
        self.money_timer.start(keep_awake._scale_ms(base, 360_000, self.brain.state))

    def _maybe_money_venture(self) -> None:
        if not self._earn_timers_ok():
            self._schedule_money_venture()
            return
        msg = money_venture.maybe_auto_venture(self.brain)
        if msg:
            self.think_aloud("💭 Geld verdienen… Jam unternimmt was!")
            if random.random() < 0.3:
                self.say(msg, user_called=False, play_sound=True)
        self._schedule_money_venture()

    def _do_money_venture(self, *, user_initiated: bool = True) -> None:
        self.think_aloud("💭 Business-Jam startet!")
        msg = money_venture.run_venture(self.brain, user_initiated=user_initiated)
        self.say(msg, user_called=user_initiated, play_sound=True, loud=True)

    def _schedule_cursor_business(self) -> None:
        t = self.brain.state.traits.get("neugierig", 0.5)
        base = int(540_000 - t * 150_000)
        self.cursor_biz_timer.start(keep_awake._scale_ms(base, 420_000, self.brain.state))

    def _maybe_cursor_business(self) -> None:
        if not self._earn_timers_ok():
            self._schedule_cursor_business()
            return
        msg = cursor_business.maybe_auto_business(self.brain)
        if msg:
            self.think_aloud("💭 Cursor + Geld + Käufer…")
            if random.random() < 0.32:
                self.say(msg, user_called=False, play_sound=True)
        self._schedule_cursor_business()

    def _schedule_gaming_fund(self) -> None:
        base = 120_000
        self.gaming_fund_timer.start(keep_awake._scale_ms(base, 180_000, self.brain.state))

    def _maybe_gaming_fund(self) -> None:
        if self.hidden_mode or self._mischief_active:
            self._schedule_gaming_fund()
            return
        msg = gaming_fund.maybe_auto_gaming_fund(self.brain)
        if msg:
            self.think_aloud("💭 Gaming-Geld für David…")
            self._show_gaming_fund_bubble()
            if random.random() < 0.35:
                self.say(msg, user_called=False, play_sound=True, loud=True)
        self._schedule_gaming_fund()

    def _show_gaming_fund_bubble(self) -> None:
        self.bubble_label.setStyleSheet(self._bubble_paypal_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(gaming_fund.format_gaming_fund_bubble(self.brain.state))
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(14_000)

    def _schedule_auto_sell(self) -> None:
        base = 100_000 if getattr(self.brain.state, "jam_active_sell_mode", True) else 150_000
        self.sell_timer.start(keep_awake._scale_ms(base, 200_000, self.brain.state))

    def _schedule_shop_heavy_promo(self) -> None:
        if not getattr(self.brain.state, "jam_shop_heavy_promo", False):
            self.shop_promo_timer.stop()
            return
        base = 22_000
        self.shop_promo_timer.start(keep_awake._scale_ms(base, 45_000, self.brain.state))

    def _maybe_shop_heavy_promo(self) -> None:
        if self.hidden_mode or self._mischief_active:
            self._schedule_shop_heavy_promo()
            return
        if not getattr(self.brain.state, "jam_shop_heavy_promo", False):
            self.shop_promo_timer.stop()
            return
        msg = shop_heavy_promo.maybe_auto_heavy_promo(self.brain)
        if msg:
            self.think_aloud("📣 Werbe-Blitz für die Jam-Seite!")
            if random.random() < 0.55:
                self.say(msg, user_called=False, play_sound=True)
        self._schedule_shop_heavy_promo()

    def _maybe_auto_sell(self) -> None:
        if self.hidden_mode or self._mischief_active:
            self._schedule_auto_sell()
            return
        if getattr(self.brain.state, "jam_shop_heavy_promo", False):
            msg = shop_heavy_promo.maybe_auto_heavy_promo(self.brain)
            if msg and random.random() < 0.5:
                self.think_aloud("📣 Shop-Werbung!")
                self.say(msg, user_called=False, play_sound=True)
            self._schedule_auto_sell()
            return
        from jam.sell_focus import is_product_build_disabled

        msg = sales_engine.maybe_auto_sell(self.brain)
        if msg:
            if is_product_build_disabled(self.brain.state):
                self.think_aloud("💭 Nur verkaufen – kein Neubau…")
            else:
                self.think_aloud("💭 Projekt bauen & verkaufen…")
            self.say(msg, user_called=False, play_sound=True)
        self._schedule_auto_sell()

    def _schedule_max_earn(self) -> None:
        base = 200_000
        self.max_earn_timer.start(keep_awake._scale_ms(base, 420_000, self.brain.state))

    def _maybe_max_earn(self) -> None:
        if not self._earn_timers_ok():
            self._schedule_max_earn()
            return
        msg = max_earn.maybe_auto_max_earn(self.brain)
        if msg:
            self.think_aloud("💰 Max-Verdienen – alle Kanäle, auch Cent!")
            if random.random() < 0.35:
                self.say(msg, user_called=False, play_sound=True)
        self._schedule_max_earn()

    def _do_deadline_sprint(self) -> None:
        self.think_aloud("⚡ Sprint bis 6.6. – Spenden & Verkauf!")
        from jam.deadline_earn import enable_deadline_sprint

        self.say(
            enable_deadline_sprint(self.brain),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_money_all_in(self) -> None:
        self.think_aloud("💰🔥 Jam setzt ALLES dran – Geld!")
        from jam.money_all_in import enable_money_all_in

        self.say(
            enable_money_all_in(self.brain),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_max_earn(self) -> None:
        self.think_aloud("💰 Jam nutzt ALLES zum Verdienen – Cent zählt!")
        msg = max_earn.run_max_earn_cycle(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _schedule_delegate(self) -> None:
        base = 180_000 if getattr(self.brain.state, "jam_delegate_mode", False) else 600_000
        self.delegate_timer.start(base + random.randint(0, 90_000))

    def _maybe_delegate_pulse(self) -> None:
        from jam.stop_all import is_all_tasks_stopped

        self._schedule_delegate()
        if is_all_tasks_stopped(self.brain.state):
            return
        from jam.platform_signup import maybe_auto_signup_sell

        plat = maybe_auto_signup_sell(self.brain)
        if plat:
            self.think_aloud("🌐 Plattform – Anmeldung & Verkauf…")
            if random.random() < 0.45:
                self.say(plat[:400], user_called=False, play_sound=True)
            return
        msg = delegate_mode.maybe_auto_mission(self.brain)
        if msg:
            self.think_aloud("🤖 Autopilot arbeitet…")
            if random.random() < 0.4:
                self.say(msg, user_called=False, play_sound=True)

    def _do_delegate_pulse(self) -> None:
        msg = delegate_mode.maybe_auto_mission(self.brain)
        if msg:
            self.say(msg[:400], user_called=False, play_sound=True)

    def _do_delegate_autopilot(self) -> None:
        self.think_aloud("🤖 David hat Freigabe – Jam übernimmt ALLES!")
        msg = delegate_mode.run_delegate_mission(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_open_jam_edge(self) -> None:
        self.think_aloud("🌐 Jam Browser – Seiten ohne Probleme!")
        from jam.jam_browser import open_jam_browser_quick

        self.say(
            open_jam_browser_quick(),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_jam_browser_autopilot(self) -> None:
        self.think_aloud("🤖 Jam Browser – ich melde mich selbst an!")
        import threading

        from jam.jam_browser import launch_browser_autopilot

        self.say(
            launch_browser_autopilot(),
            user_called=True,
            play_sound=True,
            loud=True,
        )

        def _bg() -> None:
            try:
                from jam.jam_browser_auto import enable_browser_autopilot

                enable_browser_autopilot(self.brain)
            except Exception:
                pass

        threading.Thread(target=_bg, daemon=True).start()

    def _do_open_jam_browser(self) -> None:
        self._do_open_jam_edge()

    def _do_full_contact(self) -> None:
        self.think_aloud("📨 Jam kontaktiert Kunden – du hast keine Zeit!")
        from jam.customer_contact import grant_full_contact

        self.say(grant_full_contact(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_outbound_sales(self) -> None:
        self.think_aloud("🌍 Auf Kunden zugehen – weltweit!")
        from jam.outbound_sales import run_world_outreach_mission

        self.say(
            run_world_outreach_mission(self.brain, user_initiated=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_auto_email(self) -> None:
        self.think_aloud("📧 E-Mail – Jam schreibt und schickt!")
        from jam.jam_auto_email import run_auto_email_for_need

        self.say(
            run_auto_email_for_need(self.brain, user_initiated=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_surveys(self) -> None:
        self.think_aloud("📋 Umfragen – Geld mitmachen!")
        from jam.jam_surveys import run_survey_mission

        self.say(
            run_survey_mission(self.brain, user_initiated=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_urgent_offer(self) -> None:
        self.think_aloud("⚡ Was dringend gebraucht wird – Angebot!")
        msg = urgent_offers.create_urgent_offer(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_urgent_build(self) -> None:
        self.think_aloud("👨‍💻 Kunde will es – Jam programmiert!")
        msg = urgent_offers.build_for_customer(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_urgent_sell(self) -> None:
        self.think_aloud("🛒 Fertig – verkaufen!")
        msg = urgent_offers.sell_built_offer(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_world_earn(self) -> None:
        self.think_aloud("🌍 Weltweit verdienen – PayPal global!")
        msg = world_earn.run_world_earn_round(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_platform_signup_sell(self) -> None:
        self.think_aloud("🌐 Plattformen – anmelden & programmierte Sachen verkaufen!")
        from jam.platform_signup import enable_platform_signup_sell

        msg = enable_platform_signup_sell(self.brain)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_telegram_setup(self) -> None:
        self.think_aloud("📢 Telegram-Verkaufskanal einrichten!")
        from jam.telegram_sell import setup_telegram_shop

        self.say(setup_telegram_shop(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_telegram_ads(self) -> None:
        self.think_aloud("📣 Telegram-Werbung!")
        from jam.telegram_sell import run_telegram_ads

        self.say(run_telegram_ads(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_ad_earn(self) -> None:
        self.think_aloud("📺 Klick-Seite mit Werbung!")
        from jam.ad_earn import run_ad_earn_cycle

        self.say(
            run_ad_earn_cycle(self.brain, user_initiated=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_heavy_shop_promo(self) -> None:
        self.think_aloud("📣🔥 MASSIV Werbung für die Jam-Seite!")
        msg = shop_heavy_promo.enable_heavy_promo(self.brain)
        self._schedule_shop_heavy_promo()
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_dating_site(self) -> None:
        from jam.dating_site import run_dating_mission

        self.think_aloud("💕 Jami Dating starten…")
        msg = run_dating_mission(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_master_shop(self) -> None:
        self.think_aloud("🤖 Jam-Seite – ich verkaufe und sehe die Statistik!")
        from jam.master_shop import enable_sell_only, shop_sales_report

        msg = enable_sell_only(self.brain)
        self.say(msg, user_called=True, play_sound=True, loud=True)
        self.think_aloud(shop_sales_report(self.brain)[:200])

    def _do_jam_seite_stats(self) -> None:
        self.think_aloud("📊 Was verkauft meine Seite?")
        from jam.master_shop import build_master_shop, start_shop_server, shop_sales_report

        build_master_shop(self.brain)
        start_shop_server()
        self.say(shop_sales_report(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_pc_mining_start(self) -> None:
        self.think_aloud("⛏️ Jam startet USB-Mining auf dem PC!")
        from jam.usb_miner_setup import start_pc_mining

        self.say(start_pc_mining(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_pc_mining_stop(self) -> None:
        from jam.usb_miner_setup import stop_pc_mining

        self.say(stop_pc_mining(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_pc_mining_status(self) -> None:
        from jam.usb_miner_setup import miner_status

        self.say(miner_status(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_jam_btc_wallet(self) -> None:
        from jam.usb_miner_setup import ensure_jam_bitcoin

        self.say(ensure_jam_bitcoin(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_gpu_mining(self) -> None:
        from jam.gpu_mining import start_gpu_mining_now
        from jam.usb_miner_setup import _load_config, ensure_jam_bitcoin

        self.think_aloud("🎮 GPU ohne NiceHash – lolMiner!")
        ensure_jam_bitcoin(self.brain)
        cfg = _load_config()
        self.say(
            start_gpu_mining_now(
                btc_address=cfg.get("btc_address", ""),
                power_percent=90,
                prefer_direct=True,
            ),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_gpu_mining_turbo(self) -> None:
        from jam.gpu_mining import (
            TURBO_POWER_PERCENT,
            format_earnings_report,
            estimate_gpu_earnings,
            restart_direct_gpu_mining,
        )
        from jam.usb_miner_setup import _load_config, ensure_jam_bitcoin

        self.think_aloud("🎮 GPU TURBO 110% – volle Auslastung!")
        ensure_jam_bitcoin(self.brain)
        cfg = _load_config()
        est = format_earnings_report(
            estimate_gpu_earnings(hours=8, power_percent=TURBO_POWER_PERCENT)
        )
        self.say(
            restart_direct_gpu_mining(
                btc_address=cfg.get("btc_address", ""),
                power_percent=TURBO_POWER_PERCENT,
            )
            + " "
            + est,
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_gpu_mining_nicehash(self) -> None:
        from jam.gpu_mining import start_gpu_mining_now
        from jam.usb_miner_setup import _load_config, ensure_jam_bitcoin

        ensure_jam_bitcoin(self.brain)
        cfg = _load_config()
        self.say(
            start_gpu_mining_now(
                btc_address=cfg.get("btc_address", ""),
                power_percent=90,
                prefer_direct=False,
            ),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_mining_full_setup(self) -> None:
        from jam.usb_miner_setup import setup_full_pc_mining

        self.say(setup_full_pc_mining(self.brain), user_called=True, play_sound=True, loud=True)

    def _do_shop_worldwide(self) -> None:
        self.think_aloud("🌍 Jam-Seite weltweit online!")
        from jam.shop_online import publish_shop_worldwide

        self.say(
            publish_shop_worldwide(self.brain, open_browser=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _do_firefox_captcha(self) -> None:
        self.think_aloud("🦊 Firefox – CAPTCHA & Gmail!")
        from jam.platform_auth import open_firefox_captcha_and_email

        msg = open_firefox_captcha_and_email(self.brain)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_register_all_platforms(self) -> None:
        self.think_aloud("🔐 Jam registriert auf allen Verkaufs-Plattformen…")
        from jam.platform_auth import load_credentials, run_register_all_platforms

        if not load_credentials():
            self.say(
                "David, zuerst Freigabe geben: E-Mail und Passwort sagen, "
                "z. B. „Jam, du darfst meine Anmeldedaten verwenden …“",
                user_called=True,
                play_sound=True,
                loud=True,
            )
            return
        msg = run_register_all_platforms(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_logged_in_sell(self) -> None:
        self.think_aloud("🔓 Eingeloggt – Jam hilft beim Einfügen & Verkaufen!")
        msg = active_sell.run_sell_after_login(self.brain, "ich bin eingeloggt jetzt verkaufen")
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_negotiate_price(self) -> None:
        self.think_aloud("🤝 Jam verhandelt – Rabatt oder Premium!")
        msg = price_negotiate.negotiate_last_product(
            self.brain, "verhandel den preis günstiger"
        )
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_active_sell(self) -> None:
        self.think_aloud("🛒 Aktiver Verkauf – PayPal, Gumroad, Werbung!")
        msg = active_sell.run_full_sell_mission(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_jam_sell(self) -> None:
        from jam.master_shop import is_sell_only_mode, run_master_shop_mission

        if is_sell_only_mode(self.brain.state):
            self.think_aloud("🛒 Nur noch Shop-Verkauf!")
            msg = run_master_shop_mission(self.brain, user_initiated=True)
        else:
            self.think_aloud("💭 Jam legt Preis fest und verkauft!")
            msg = sales_engine.run_sale_project(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _schedule_auto_ops(self) -> None:
        base = 200_000
        self.ops_timer.start(keep_awake._scale_ms(base, 300_000, self.brain.state))

    def _maybe_auto_ops(self) -> None:
        if self.hidden_mode or self._mischief_active:
            self._schedule_auto_ops()
            return
        msg = auto_ops.maybe_auto_ops(self.brain)
        if msg:
            self.think_aloud("💭 Download & Upload – alle Kanäle!")
            self.say(msg, user_called=False, play_sound=True)
        self._schedule_auto_ops()

    def _schedule_care(self) -> None:
        base = 130_000
        self.care_timer.start(keep_awake._scale_ms(base, 200_000, self.brain.state))

    def _maybe_care(self) -> None:
        if self.hidden_mode:
            self._schedule_care()
            return
        msg = care.maybe_auto_care(self.brain)
        if msg:
            self.think_aloud("💭 David soll's gut haben…")
            self._show_care_bubble()
            self.say(msg, user_called=False, play_sound=True)
        self._schedule_care()

    def _show_care_bubble(self) -> None:
        self.bubble_label.setStyleSheet(self._bubble_paypal_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(care.format_care_bubble(self.brain.state))
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(12_000)

    def _maybe_paypal_reward_remind(self) -> None:
        msg = paypal_reward.maybe_remind_reward(self.brain)
        if msg and random.random() < 0.35:
            self.say(msg, user_called=False, play_sound=False)

    def _celebrate_paypal_reward(self, tier: str) -> None:
        self.bubble_label.setStyleSheet(self._bubble_paypal_style)
        self.bubble_label.setMaximumWidth(300)
        log = getattr(self.brain.state, "paypal_income_log", []) or []
        amt = (
            float(log[-1].get("amount", 0))
            if log and isinstance(log[-1], dict)
            else 0.0
        )
        self.bubble_label.setText(
            paypal_reward.format_reward_bubble(self.brain.state, amt)
        )
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(16_000)
        self._start_dance(announce=False)
        if tier in ("big", "huge"):
            self._launch_fireworks()
        if tier == "huge":
            QTimer.singleShot(800, lambda: self._launch_fireworks())

    def _do_paypal_reward(self) -> None:
        amount_str, ok1 = QInputDialog.getText(
            self,
            "PayPal Geld da",
            "Wie viel Euro sind auf PayPal angekommen?",
            text="5",
        )
        if not ok1 or not amount_str.strip():
            return
        try:
            amount = float(amount_str.strip().replace(",", "."))
        except ValueError:
            self.say("Das versteh ich nicht – gib eine Zahl ein, z. B. 10", user_called=True)
            return
        note, ok2 = QInputDialog.getText(
            self, "PayPal", "Notiz (optional, z. B. Spende)", text=""
        )
        note = note if ok2 else ""
        speech, tier = paypal_reward.reward_jam_for_paypal(
            self.brain, amount, source="menu", note=note
        )
        if tier == "none":
            self.say(speech, user_called=True)
            return
        self.think_aloud("🎁 PayPal – Belohnung für Jam!")
        self._celebrate_paypal_reward(tier)
        self.say(speech, user_called=True, play_sound=True, loud=True)

    def _toggle_night_earn(self) -> None:
        if keep_awake.is_night_earn_mode(self.brain.state):
            msg = keep_awake.disable_night_earn(self.brain)
        else:
            msg = keep_awake.enable_night_earn(self.brain, persistent_power=True)
            self._refresh_keep_awake()
        self.say(msg, user_called=True, play_sound=True)

    def _do_care_check(self) -> None:
        self.think_aloud("💭 Wie geht es dir?")
        msg = care.run_care_check(self.brain, user_initiated=True)
        self._show_care_bubble()
        self.say(msg, user_called=True, play_sound=True)

    def _do_find_play_friend(self) -> None:
        self.think_aloud("💭 Spiel-Freund für David!")
        msg = care.find_play_friend(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_full_ops(self) -> None:
        self.think_aloud("💭 Jam nutzt alle Möglichkeiten!")
        msg = auto_ops.run_full_ops_cycle(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _toggle_auto_fetch(self) -> None:
        s = self.brain.state
        s.jam_auto_fetch_enabled = not getattr(s, "jam_auto_fetch_enabled", True)
        self.brain.save()
        on = s.jam_auto_fetch_enabled
        self.say(
            f"Auto-Download ohne Fragen: {'AN' if on else 'AUS'}.",
            user_called=True,
            play_sound=True,
        )

    def _do_gaming_fund(self) -> None:
        self.think_aloud("💭 Echtes Geld fürs Zocken – PayPal!")
        msg = gaming_fund.run_gaming_earn_cycle(self.brain, user_initiated=True)
        self._show_gaming_fund_bubble()
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _pc_activity_learn_tick(self) -> None:
        pc_activity_learn.tick(self.brain)

    def _do_reach_build(self) -> None:
        self.think_aloud("📡 Reichweite – Shop weltweit!")
        self.say(
            reach_build.build_reach(self.brain, open_browser=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _open_torchwood_mod(self) -> None:
        from jam import pc_activity_learn

        pc_activity_learn.enable_learn(self.brain)
        torchwood_moderator.enable_watch(self.brain)
        self.say(
            torchwood_moderator.open_torchwood(self.brain, with_chat_read=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _torchwood_chat_poll_tick(self) -> None:
        if getattr(self.brain.state, "jam_torchwood_chat_read", True):
            torchwood_moderator.poll_chat_from_file(self.brain)

    def _toggle_pc_activity_learn(self) -> None:
        if pc_activity_learn.is_enabled(self.brain.state):
            self.say(pc_activity_learn.disable_learn(self.brain), user_called=True)
        else:
            self.say(
                pc_activity_learn.enable_learn(self.brain),
                user_called=True,
                play_sound=True,
            )

    def _maybe_pc_control(self) -> None:
        if not pc_control.is_enabled(self.brain.state):
            return
        msg = pc_control.maybe_auto_help(self.brain)
        if msg and random.random() < 0.25:
            self.say(msg, user_called=False)

    def _toggle_pc_control(self) -> None:
        if pc_control.is_enabled(self.brain.state):
            self.say(pc_control.disable_control(self.brain), user_called=True)
        else:
            self.say(pc_control.enable_control(self.brain), user_called=True, play_sound=True)

    def _do_cursor_business(self, *, user_initiated: bool = True) -> None:
        self.think_aloud("💭 Ich lerne Cursor, baue was und suche Käufer!")
        msg = cursor_business.run_cursor_business(
            self.brain, user_initiated=user_initiated
        )
        self._show_cursor_business_bubble(msg)
        self.say(msg, user_called=user_initiated, play_sound=True, loud=True)

    def _show_cursor_business_bubble(self, speech: str) -> None:
        log = getattr(self.brain.state, "cursor_business_log", []) or []
        if log and isinstance(log[-1], dict):
            e = log[-1]
            text = (
                "🧑‍💻 Cursor-Business\n"
                f"{e.get('title', 'Produkt')}\n"
                f"~{e.get('price', '?')} €\n"
                f"Käufer: {', '.join(l['name'] for l in e.get('leads', [])[:3])}\n"
                f"Ordner: {Path(e.get('path', '')).name}"
            )
        else:
            text = speech[:200]
        self.bubble_label.setStyleSheet(self._bubble_studio_style)
        self.bubble_label.setMaximumWidth(320)
        self.bubble_label.setText(text)
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(15_000)

    def _maybe_learn_sentences(self) -> None:
        if self.hidden_mode or self._gaming or self._mischief_active:
            self._schedule_sentence_learn()
            return
        from jam import sentence_learn

        msg = internet_learn.maybe_auto_learn(self.brain)
        if msg:
            self.think_aloud("💭 Ich lern online, was ich will…")
            if random.random() < 0.28:
                self.say(msg, user_called=False, play_sound=True)
        self._schedule_sentence_learn()

    def _maybe_cursor_self_improve(self) -> None:
        if self.hidden_mode or self._mischief_active or self._fart_busy:
            self._schedule_cursor_improve()
            return
        msg = cursor_lab.maybe_auto_self_improve(self.brain)
        if msg:
            self.think_aloud("💭 Cursor… ich entwickle mich weiter!")
            self.say(msg, user_called=False, play_sound=True)
        self._schedule_cursor_improve()

    def _do_learn_sentences(self) -> None:
        self._do_internet_learn_free(user_initiated=True)

    def _do_internet_learn_free(self, *, user_initiated: bool = True) -> None:
        self.think_aloud("💭 Internet – ich lern, was ich will!")
        msg = internet_learn.learn_session(
            self.brain, user_initiated=user_initiated, open_browser=True
        )
        self.say(msg, user_called=user_initiated, play_sound=True)

    def _do_cursor_self_improve(self) -> None:
        ok, msg, _ = cursor_lab.create_self_improve_session(self.brain)
        self.think_aloud("💭 Cursor… Selbst-Programmierung!")
        self.say(msg, user_called=True, play_sound=ok)
        if ok:
            self._refresh_appearance()

    def _maybe_learn_web(self) -> None:
        if self.hidden_mode or self._gaming or self._mischief_active:
            self._schedule_learn()
            return
        from jam import web_learn

        msg = internet_learn.maybe_auto_learn(self.brain)
        if msg:
            self.think_aloud("💭 Lern-Website im Netz…")
            if random.random() < 0.22:
                self.say(msg, play_sound=True)
        self._schedule_learn()

    def _music_tick(self) -> None:
        title = foreground_title()
        active = music.is_music_playing(title)
        if active and not self._was_music:
            line = music.on_music_detected(self.brain, title)
            if line:
                self._say_music(line)
            else:
                self._say_music(music.pick_sing_line(self.brain, title))
            self._start_dance(announce=False)
            if "youtube" not in title.lower() and random.random() < 0.35:
                if self.brain.state.stage >= 1:
                    QTimer.singleShot(
                        1200,
                        lambda: self._say_music(
                            music.open_youtube_music(self.brain) or ""
                        ),
                    )
        elif active and self._dancing and music.should_sing_while_playing(
            self.brain.state
        ):
            self._say_music(music.pick_sing_line(self.brain, title))
        elif not active and self._was_music:
            self._stop_dance()
        self._was_music = active
        self._music_active = active

    def _start_dance(self, *, announce: bool = True) -> None:
        if self._mischief_active or self.hidden_mode:
            return
        self._dancing = True
        self._dance_phase = 0.0
        self.brain.state.dance_sessions += 1
        self.brain.save()
        if announce:
            self._say_music(music.pick_sing_line(self.brain))

    def _stop_dance(self) -> None:
        self._dancing = False
        self._restore_walk_speed()

    def _launch_fireworks(self) -> None:
        if self._fireworks_show is not None:
            try:
                self._fireworks_show.close()
            except RuntimeError:
                pass
            self._fireworks_show = None
        show = fireworks.launch_fireworks_show(self)
        if show is not None:
            self._fireworks_show = show
            show.destroyed.connect(self._on_fireworks_closed)
        self.brain.add_xp(6)

    def _on_fireworks_closed(self, *_args) -> None:
        self._fireworks_show = None
        self._ensure_visible()

    def _do_fireworks(self) -> None:
        self._launch_fireworks()
        self.say(fireworks.pick_line(), play_sound=True)

    def _idle_show_blocked(self) -> bool:
        return (
            self.hidden_mode
            or self._mischief_active
            or self._fart_busy
            or self._gaming
            or self._fireworks_show is not None
        )

    def _maybe_idle_show(self) -> None:
        now = time.time()
        if self._idle_show_kind and now >= self._idle_show_until:
            self._end_idle_show()
        if self._idle_show_blocked() or self._tiktok_hearts_show is not None:
            return
        last = max(self._last_idle_show_at, getattr(self.brain.state, "last_idle_show_at", 0.0))
        if not show_animations.should_run_idle_show(
            self.brain.state,
            last_show_at=last,
            now=now,
            blocked=False,
        ):
            return
        self._start_idle_show(show_animations.pick_show(self.brain.state))

    def _start_idle_show(self, kind: str, *, manual: bool = False) -> None:
        if self._idle_show_blocked() and not manual:
            return
        self._end_idle_show()
        dur = show_animations.duration_for(kind)
        self._idle_show_kind = kind
        self._idle_show_until = time.time() + dur
        self._idle_show_phase = 0.0
        self._last_idle_show_at = time.time()
        self.brain.state.last_idle_show_at = self._last_idle_show_at
        self.brain.state.last_idle_show_kind = kind
        self.brain.state.idle_shows_count = (
            getattr(self.brain.state, "idle_shows_count", 0) + 1
        )
        self.brain.add_xp(4)
        self.brain.save()
        self.think_aloud(show_animations.thought_for(kind))
        self._show_bubble(show_animations.bubble_line(), thought=False)
        self.bubble_timer.start(6000)

        if kind == "fireworks":
            self._launch_fireworks()
        elif kind == "dance_burst":
            self._start_dance()
        elif kind in ("star_parade", "confetti_rain", "rainbow_pulse"):
            show = show_animations.launch_particle_show(self, kind)
            if show is not None:
                self._particle_show = show
                show.destroyed.connect(self._on_particle_show_closed)
        elif kind in ("spin_party", "bounce_loop"):
            self.vx *= 1.15
            self.vy *= 1.15

    def _end_idle_show(self) -> None:
        kind = self._idle_show_kind
        self._idle_show_kind = None
        self._idle_show_until = 0.0
        self._idle_show_phase = 0.0
        if kind == "dance_burst":
            self._stop_dance()
        if self._particle_show is not None:
            try:
                self._particle_show.close()
            except RuntimeError:
                pass
            self._particle_show = None
        self._restore_walk_speed()
        self._ensure_visible()

    def _on_particle_show_closed(self, *_args) -> None:
        self._particle_show = None
        self._ensure_visible()

    def _on_tiktok_hearts_closed(self, *_args) -> None:
        self._tiktok_hearts_show = None
        self._ensure_visible()

    def _tick_idle_show(self) -> None:
        if not self._idle_show_kind:
            return
        if time.time() >= self._idle_show_until:
            self._end_idle_show()

    def _do_idle_show_menu(self) -> None:
        kind = show_animations.pick_show(self.brain.state)
        self._start_idle_show(kind, manual=True)

    def _wander_dance(self, geo) -> None:
        self._dance_phase += 0.07
        self._bob_phase += 0.16
        speed = 1.15 + self.brain.state.stage * 0.06
        self.vx = math.sin(self._dance_phase) * speed
        self.vy = math.cos(self._dance_phase * 0.9) * (speed * 0.55)
        if self.vx < 0:
            self._face_left = True
        else:
            self._face_left = False
        self._pos_x += self.vx
        self._pos_y += self.vy
        hop = self._walk_hop_offset()
        if self._pos_x <= geo.left() or self._pos_x + self.width() >= geo.right():
            self._dance_phase += math.pi * 0.5
        if self._pos_y <= geo.top() or self._pos_y + self.height() >= geo.bottom():
            self._dance_phase += math.pi * 0.35
        self._pos_x = max(geo.left(), min(self._pos_x, geo.right() - self.width()))
        self._pos_y = max(geo.top(), min(self._pos_y, geo.bottom() - self.height()))
        self.move(int(self._pos_x), int(self._pos_y - hop))
        self._layout_sprite_bob()

    def _think_tick(self) -> None:
        if self.hidden_mode or self._mischief_active or self._fart_busy:
            self._schedule_think()
            return
        ctx = self._jam_context()
        decision = mind.think(self.brain, ctx)
        smarter = ai_optional.think_reply(
            self.brain, decision.thought, ctx.window_title or "Desktop"
        )
        if smarter:
            decision.thought = smarter
            self.brain.state.last_thought = smarter
        self.speak_thought(decision.thought, loud=decision.loud)
        QTimer.singleShot(1800, lambda: self._act_on_decision(decision))
        self._schedule_think()

    def _schedule_think(self) -> None:
        pause_ms = int(MIN_LINE_GAP_SEC * 1000)
        self.think_timer.start(pause_ms + random.randint(30_000, 90_000))

    def _act_on_decision(self, decision) -> None:
        action = decision.action
        if decision.speak:
            spoken = mind.thought_to_speech(decision.thought)
            if decision.speak.strip() != spoken:
                self.say(decision.speak, loud=decision.loud)
        if action == "mischief" and self.brain.state.stage >= 1:
            self._start_mischief()
        elif action == "learn":
            self._learn_web()
        elif action == "play_urge":
            self._do_play()
        elif action == "help" and self.brain.state.stage >= 2:
            from jam.owner import owner_name

            self.bubble.emit(
                f"{owner_name(self.brain.state)}: Rechtsklick → Befehl, wenn du Hilfe brauchst."
            )
        elif action == "fart":
            self._do_fart(from_mind=True)
        elif action == "cursor_learn":
            self._do_cursor_learn(from_mind=True)
        elif action == "cursor_self_improve":
            self._do_cursor_self_improve()
        elif action == "internet_learn":
            self._do_internet_learn_free(user_initiated=False)
        elif action == "money_venture":
            self._do_money_venture(user_initiated=False)
        elif action == "cursor_business":
            self._do_cursor_business(user_initiated=False)
        elif action == "gaming_fund":
            self._do_gaming_fund()
        elif action == "jam_sale":
            self._do_jam_sell()
        elif action == "auto_ops":
            self._do_full_ops()
        elif action == "owner_care":
            self._do_care_check()
        elif action == "play_friend":
            self._do_find_play_friend()
        elif action == "sentence_learn":
            self._do_learn_sentences()
        elif action == "music_dance":
            self._start_dance(announce=False)
            if decision.speak:
                self._say_music(decision.speak)
            else:
                self._say_music(music.pick_sing_line(self.brain))
        elif action == "youtube_music":
            self._start_dance(announce=False)
            self._say_music(music.open_youtube_music(self.brain, user_pick=True))
        elif action == "tiktok_friends":
            self._present_on_tiktok(
                tiktok.present_on_tiktok(self.brain, user_asked=False)
            )
        elif action == "tiktok_connect":
            self._present_tiktok_connect(
                tiktok_account.connect_account(self.brain, user_asked=False)
            )
        elif action == "close_fortnite":
            self.think_aloud("💭 Fortnite… tschüss!")
            self.say(game_launcher.close_game(self.brain, "fortnite"))
        elif action == "gaming_roast":
            self.think_aloud("💭 Ich meckere laut zum Zocken…")
            self._say_gaming_roast(on_start=False)
        elif action == "gaming_cheat":
            self._say_gaming_roast(on_start=False, accuse_cheat=True)
        self.brain.save()

    def _refresh_appearance(self) -> None:
        self._resize_to_scale()
        self._update_sprite_visual()

    def _do_cursor_learn(self, from_mind: bool = False) -> None:
        if from_mind:
            self.think_aloud("💭 Cursor-Zeit… ich bastel ein Lern-Projekt!")
        ok, msg, _path = cursor_lab.create_learning_project(self.brain)
        self.say(msg)
        if ok:
            self._refresh_appearance()
            self.brain.state.happiness = min(1.0, self.brain.state.happiness + 0.12)
            self.brain.save()

    def _schedule_fart(self) -> None:
        self.fart_timer.start(random.randint(90_000, 200_000))

    def _schedule_fortnite_close(self) -> None:
        self.fortnite_timer.start(random.randint(120_000, 300_000))

    def _maybe_close_fortnite(self) -> None:
        if self.hidden_mode or self._mischief_active or self._fart_busy:
            self._schedule_fortnite_close()
            return
        if not game_launcher.is_fortnite_running():
            self._schedule_fortnite_close()
            return
        if random.random() > game_launcher.close_game_chance(self.brain.state):
            self._schedule_fortnite_close()
            return
        self.think_aloud(random.choice([
            "💭 Fortnite nervt Jam…",
            "💭 Ich mach Fortnite zu, hehe.",
            "💭 Frech-Idee: Fortnite aus!",
        ]))
        self.say(game_launcher.close_game(self.brain, "fortnite"))
        self._schedule_fortnite_close()

    def _maybe_fart(self) -> None:
        if self._fart_busy or self.hidden_mode or self._mischief_active:
            self._schedule_fart()
            return
        if random.random() > fart.fart_chance(self.brain.state):
            self._schedule_fart()
            return
        self._do_fart()
        self._schedule_fart()

    def _play_fart_laugh_audio(self, loud: bool) -> None:
        """Laut pupsen + lachen auf dem Qt-Hauptthread."""
        voice_assets.play_fart_and_laugh_sync(loud=loud)
        QTimer.singleShot(80, self._finish_fart_laugh)

    def _finish_fart_laugh(self) -> None:
        line = fart.pick_fart_laugh(self.brain.state)
        self.say(line, loud=False, play_sound=False)
        self.brain.state.add_xp(6)
        self.brain.state.happiness = min(1.0, self.brain.state.happiness + 0.1)
        self._fart_busy = False
        self.brain.save()

    def _do_fart(self, from_mind: bool = False) -> None:
        if self._fart_busy:
            return
        self._fart_busy = True
        if from_mind:
            self.think_aloud(fart.pick_fart_thought())

        self._show_bubble(fart.pick_fart_bubble(self.brain.state), thought=False)
        self.bubble_timer.start(4500)
        self._play_fart_audio.emit(fart.fart_is_loud(self.brain.state))

    def _face_walk_direction(self) -> None:
        face_left = self.vx < 0
        if face_left != self._face_left:
            self._update_sprite_visual(face_left)

    def _update_facing(self) -> None:
        if self._mischief_active:
            cursor = QCursor.pos()
            center = self.frameGeometry().center()
            face_left = cursor.x() < center.x()
        elif self._gaming or (abs(self.vx) > 0.3):
            face_left = self.vx < 0
        else:
            return
        if face_left != self._face_left:
            self._update_sprite_visual(face_left)

    def _screen_geo(self):
        screen = QApplication.primaryScreen()
        return screen.availableGeometry() if screen else None

    def _enter_spectator_mode(self) -> None:
        geo = self._screen_geo()
        if not geo:
            return
        self._spectator_y = geo.bottom() - self.height() - 28
        self.vy = 0
        speed = 2.6 + self.brain.state.stage * 0.15
        self.vx = speed if self.vx >= 0 else -speed
        self.move(self.pos().x(), self._spectator_y)
        self._sync_pos_from_window()
        self.setWindowOpacity(1.0)
        self._face_walk_direction()
        if self.brain.state.stage >= 1 and random.random() < 0.35:
            self.think_aloud("Zock zu – ich lauf unten auf dem Desktop mit!")

    def _exit_spectator_mode(self) -> None:
        self._spectator_y = None
        self.vx = random.choice([-1, 1]) * 1.2
        self.vy = random.choice([-1, 1]) * 1.0
        self._face_walk_direction()

    def _maybe_mischief(self) -> None:
        if self._mischief_active or self.hidden_mode:
            self._schedule_mischief()
            return
        if self.brain.state.stage < 1:
            self._schedule_mischief()
            return
        if random.random() > frech_probability(self.brain.state):
            self._schedule_mischief()
            return
        self._start_mischief()

    def _start_mischief(self) -> None:
        session = icon_shaker.start_session(random.randint(1, 3))
        if not session:
            self._schedule_mischief()
            return

        self._mischief_active = True
        self._shake_session = session
        spot = icon_shaker.jam_position_near_icons(session)
        if spot:
            self.move(spot[0], spot[1])
            self._sync_pos_from_window()
        self._update_facing()
        self.say(freich_line(self.brain.state))
        self.shake_timer.start(140)
        self._schedule_mischief()

    def _schedule_mischief(self) -> None:
        self.mischief_timer.start(random.randint(80_000, 180_000))

    def _shake_tick(self) -> None:
        self._update_facing()
        session = self._shake_session
        if not session:
            return
        session.tick()
        if session.done:
            session.restore()
            self._shake_session = None
            self._mischief_active = False
            self.shake_timer.stop()
            self._face_left = False
            self._update_sprite_visual(False)

    def _wander(self) -> None:
        self._tick_idle_show()
        if self.hidden_mode or self._mischief_active or self._fart_busy:
            return
        geo = self._screen_geo()
        if not geo:
            return
        if self._dancing:
            self._wander_dance(geo)
        elif self._gaming:
            self._wander_spectator(geo)
        else:
            self._wander_free(geo)

    def _wander_spectator(self, geo) -> None:
        """Beim Zocken: nur horizontal unten auf dem Desktop hin und her."""
        y = self._spectator_y
        if y is None:
            y = geo.bottom() - self.height() - 28
            self._spectator_y = y
        self._pos_x += self.vx
        self._pos_y = float(y)
        if self._pos_x <= geo.left():
            self._pos_x = float(geo.left())
            self.vx = abs(self.vx)
            self._face_walk_direction()
        elif self._pos_x + self.width() >= geo.right():
            self._pos_x = float(geo.right() - self.width())
            self.vx = -abs(self.vx)
            self._face_walk_direction()
        hop = self._walk_hop_offset()
        self.move(int(self._pos_x), int(self._pos_y) - hop)
        self._layout_sprite_bob()

    def _wander_free(self, geo) -> None:
        self._pos_x += self.vx
        self._pos_y += self.vy
        hop = self._walk_hop_offset()
        if self._pos_x <= geo.left():
            self._pos_x = float(geo.left())
            self.vx *= -1
            self._face_walk_direction()
        elif self._pos_x + self.width() >= geo.right():
            self._pos_x = float(geo.right() - self.width())
            self.vx *= -1
            self._face_walk_direction()
        if self._pos_y <= geo.top():
            self._pos_y = float(geo.top())
            self.vy *= -1
        elif self._pos_y + self.height() >= geo.bottom():
            self._pos_y = float(geo.bottom() - self.height())
            self.vy *= -1
        self.move(int(self._pos_x), int(self._pos_y - hop))
        self._layout_sprite_bob()

    def _ensure_visible(self) -> None:
        """Sichtbar – außer Jam ist im Zimmer oder kurz versteckt."""
        if getattr(self.brain.state, "jam_in_room", False):
            return
        if self.hidden_mode and self._hide_return_timer.isActive():
            return
        if self.hidden_mode:
            self._come_out_short_hide()
            return
        self.show()
        self.raise_()
        self.setWindowOpacity(1.0)

    def _cancel_hide_return(self) -> None:
        if self._hide_return_timer.isActive():
            self._hide_return_timer.stop()

    def _go_to_room(self) -> bool:
        """Sprache/Menü: Jam verschwindet bis „komm raus“."""
        self._cancel_hide_return()
        if not self.hidden_mode:
            self._room_saved_pos = (self._pos_x, self._pos_y)
        self.hidden_mode = True
        self.brain.state.jam_in_room = True
        spot = hide_behind_folder()
        screen = QApplication.primaryScreen()
        geo = screen.availableGeometry() if screen else None
        if spot:
            self.move(spot[0], spot[1])
        elif geo is not None:
            self.move(geo.right() - self.width() - 4, geo.bottom() - self.height() - 4)
        self._sync_pos_from_window()
        self.setWindowOpacity(0.0)
        self.hide()
        self.brain.save()
        return True

    def _come_out_from_room(self) -> None:
        """Sprache: Jam zeigt sich wieder."""
        self._cancel_hide_return()
        self.hidden_mode = False
        self.brain.state.jam_in_room = False
        self.setWindowOpacity(1.0)
        self.show()
        if self._room_saved_pos:
            x, y = self._room_saved_pos
            self.move(int(x), int(y))
            self._sync_pos_from_window()
        else:
            self._place_center()
        self.raise_()
        self.activateWindow()
        self.brain.save()

    def _menu_go_room(self) -> None:
        self._go_to_room()
        self.say(room.say_go_room(self.brain.state, ok=True))

    def _do_hide(self) -> None:
        """Menü: kurz hinter Ordner, kommt von selbst zurück."""
        spot = hide_behind_folder()
        if not spot:
            return
        self._cancel_hide_return()
        self.brain.state.jam_in_room = False
        self.hidden_mode = True
        self.setWindowOpacity(0.35)
        self.show()
        self.move(spot[0], spot[1])
        self._sync_pos_from_window()
        self._hide_return_timer.start(random.randint(8000, 20_000))

    def _come_out_short_hide(self) -> None:
        if getattr(self.brain.state, "jam_in_room", False):
            return
        self.hidden_mode = False
        self.setWindowOpacity(1.0)
        self.show()
        self.raise_()
        self.say(random.choice(["Buh!", "Da bin ich wieder!", "Hab mich kurz versteckt!"]))

    def _brain_tick(self) -> None:
        self.brain.tick()
        self.brain.save()
        if self.brain.state.hunger > 0.9:
            from jam.owner import ask_owner

            hunger_line = ask_owner(self.brain.state, "hunger")
            if not is_repeat_line(self.brain.state, hunger_line):
                self.say(hunger_line)

    def _say_gaming_roast(self, *, on_start: bool = False, accuse_cheat: bool = False) -> None:
        if not self._gaming or self._mischief_active or self._fart_busy:
            return
        if self.brain.state.stage < 1:
            return
        now = time.time()
        if now - getattr(self.brain.state, "last_gaming_roast", 0) < MIN_LINE_GAP_SEC:
            return
        from jam.speech import gaming_cheat_line, gaming_roast_line

        if accuse_cheat:
            line = gaming_cheat_line(self.brain.state)
        else:
            line = gaming_roast_line(self.brain.state)
        if is_repeat_line(self.brain.state, line):
            return
        self.say(line, loud=True, play_sound=True)
        self.brain.state.last_gaming_roast = now
        self.brain.state.gaming_roasts = getattr(self.brain.state, "gaming_roasts", 0) + 1
        self.brain.state.add_xp(5)
        self.brain.state.remember(f"Gaming-Roast: {line[:80]}")
        self.brain.save()

    def _game_tick(self) -> None:
        apex_focus.maybe_keep_apex_foreground(self.brain)
        title = foreground_title()
        gaming = looks_like_gaming(title)
        if gaming and not self._was_gaming:
            self._enter_spectator_mode()
            QTimer.singleShot(3500, lambda: self._say_gaming_roast(on_start=True))
        elif not gaming and self._was_gaming:
            self._exit_spectator_mode()
        self._was_gaming = gaming
        self._gaming = gaming
        idle = time.time() - self._session_start
        if gaming and should_comment(
            idle,
            self.brain.state.stage,
            just_started=False,
            last_spoken_at=getattr(self.brain.state, "last_line_spoken_at", 0.0),
        ):
            coach = apex_coach.maybe_apex_coach_comment(self.brain, title)
            if coach:
                self.say(coach, user_called=False)
            else:
                self._say_gaming_roast(on_start=False)

    def _show_listening_pulse(self) -> None:
        self.bubble.emit(voice_assistant.listening_ack(self.brain.state))
        self.bubble_label.setStyleSheet(self._bubble_normal_style)
        self.bubble_label.setMaximumWidth(220)
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(1600)

    def wake(self, heard: str) -> None:
        self.brain.state.last_interaction = time.time()
        if self._idle_show_kind:
            self._end_idle_show()
        voice_assistant.touch_conversation_session()
        self._show_listening_pulse()

        heard = game_launcher.normalize_speech(heard)
        cmd = voice_assistant.prepare_heard(heard)
        heard_l = (cmd or heard).lower()

        if room.hears_come_out(heard) or room.hears_come_out(cmd):
            self._come_out_from_room()
            self.say(room.say_come_out(self.brain.state), user_called=True, play_sound=True)
            return

        if room.hears_go_room(heard) or room.hears_go_room(cmd):
            self._go_to_room()
            self.say(
                room.say_go_room(self.brain.state, ok=True),
                user_called=True,
                play_sound=True,
            )
            return

        alexa_reply = voice_assistant.route_or_none(self.brain, heard)
        if alexa_reply:
            self.say(alexa_reply, user_called=True, play_sound=True)
            return

        stop_reply = stop_all.handle_speech(self.brain, heard)
        if stop_reply:
            self._apply_stop_all_timers()
            self.say(stop_reply, user_called=True, play_sound=True, loud=True)
            return

        new_owner = parse_owner_from_speech(heard)
        if new_owner:
            self.say(set_owner_name(self.brain.state, new_owner), user_called=True)
            self.brain.save()
            return
        edu = upbringing.parse_speech(heard)
        if edu:
            self.say(upbringing.apply_parse(self.brain.state, edu))
            self.brain.save()
            return

        torch_reply = torchwood_moderator.handle_speech(self.brain, heard)
        if torch_reply:
            self.say(torch_reply, user_called=True, play_sound=True)
            return

        pc_learn_reply = pc_activity_learn.handle_speech(self.brain, heard)
        if pc_learn_reply:
            self.say(pc_learn_reply, user_called=True, play_sound=True)
            return

        pc_reply = pc_control.handle_speech(self.brain, heard)
        if pc_reply:
            self.say(pc_reply, user_called=True, play_sound=True)
            return
        if pc_control.is_enabled(self.brain.state):
            pc_act = pc_control.execute_pc(self.brain, heard)
            if pc_act:
                self.say(pc_act, user_called=True, play_sound=True)
                return

        if fireworks.hears_feuerwerk(heard_l):
            self._launch_fireworks()
            self.say(fireworks.pick_line(), user_called=True, play_sound=True)
            return

        if tiktok_hearts.hears_tiktok_hearts(heard_l):
            self._present_tiktok_hearts()
            return

        free_reply = free_orders.handle_speech(self.brain, heard)
        if free_reply:
            self._present_free_order(free_reply)
            return

        if internet_learn.hears_free_learn(heard):
            msg = internet_learn.learn_session(
                self.brain, user_initiated=True, open_browser=True
            )
            self.say(msg, user_called=True, play_sound=True)
            return

        stream_reply = stream_prep.handle_speech(self.brain, heard)
        if stream_reply:
            self._show_stream_bubble()
            self.say(stream_reply, user_called=True, play_sound=True, loud=True)
            return

        mining_reply = mining_report.handle_speech(self.brain, heard)
        if mining_reply:
            self.say(mining_reply, user_called=True, play_sound=True)
            return

        sell_focus_reply = sell_focus.handle_speech(self.brain, heard)
        if sell_focus_reply:
            self.say(sell_focus_reply, user_called=True, play_sound=True)
            return

        earn_reply = earn_report.handle_speech(self.brain, heard)
        if earn_reply:
            self.say(earn_reply, user_called=True, play_sound=True, loud=True)
            return

        extreme_reply = extreme_learn.handle_speech(self.brain, heard)
        if extreme_reply:
            self.say(extreme_reply, user_called=True, play_sound=True, loud=True)
            return

        max_earn_reply = max_earn.handle_speech(self.brain, heard)
        if max_earn_reply:
            self.say(max_earn_reply, user_called=True, play_sound=True, loud=True)
            return

        reach_reply = reach_build.handle_speech(self.brain, heard)
        if reach_reply:
            self.think_aloud("📡 Reichweite aufbauen!")
            self.say(reach_reply, user_called=True, play_sound=True, loud=True)
            return

        world_reply = world_earn.handle_speech(self.brain, heard)
        if world_reply:
            self.say(world_reply, user_called=True, play_sound=True, loud=True)
            return

        always_reply = always_on.handle_speech(self.brain, heard)
        if always_reply:
            self.say(always_reply, user_called=True, play_sound=True)
            return

        from jam import platform_signup, telegram_sell

        telegram_reply = telegram_sell.handle_speech(self.brain, heard)
        if telegram_reply:
            self.think_aloud("📢 Telegram Shop & Werbung!")
            self.say(telegram_reply, user_called=True, play_sound=True, loud=True)
            return

        platform_reply = platform_signup.handle_speech(self.brain, heard)
        if platform_reply:
            self.think_aloud("🌐 Plattformen – anmelden & verkaufen!")
            self.say(platform_reply, user_called=True, play_sound=True, loud=True)
            return

        night_focus_reply = night_focus.handle_speech(self.brain, heard)
        if night_focus_reply:
            self.say(night_focus_reply, user_called=True, play_sound=True, loud=True)
            return

        night_reply = keep_awake.handle_speech(self.brain, heard)
        if night_reply:
            self.say(night_reply, user_called=True, play_sound=True, loud=True)
            return

        discord_reply = discord_control.handle_speech(self.brain, heard)
        if discord_reply:
            self.think_aloud("💬 Discord – Bot & Skripte!")
            self.say(discord_reply, user_called=True, play_sound=True, loud=True)
            return

        website_reply = website_control.handle_speech(self.brain, heard)
        if website_reply:
            self.think_aloud("🌐 Moderne Webseiten!")
            self.say(website_reply, user_called=True, play_sound=True, loud=True)
            return

        lotto_reply = eurojackpot_tip.handle_speech(self.brain, heard)
        if lotto_reply:
            self.think_aloud("🎱 Eurojackpot-Tipp!")
            self.say(lotto_reply, user_called=True, play_sound=True, loud=True)
            return

        gpu_reply = gpu_mining.handle_speech(self.brain, heard)
        if gpu_reply:
            self.think_aloud("🎮 GPU-Mining!")
            self.say(gpu_reply, user_called=True, play_sound=True, loud=True)
            return

        care_reply = care.handle_speech(self.brain, heard)
        if care_reply:
            self._show_care_bubble()
            self.say(care_reply, user_called=True, play_sound=True, loud=True)
            return

        ops_reply = auto_ops.handle_speech(self.brain, heard)
        if ops_reply:
            self.say(ops_reply, user_called=True, play_sound=True, loud=True)
            return

        negotiate_reply = price_negotiate.handle_speech(self.brain, heard)
        if negotiate_reply:
            self.think_aloud("🤝 Jam verhandelt den Preis!")
            self.say(negotiate_reply, user_called=True, play_sound=True, loud=True)
            return

        urgent_reply = urgent_offers.handle_speech(self.brain, heard)
        if urgent_reply:
            self.think_aloud("⚡ Dringendes Angebot – erst verkaufen, dann bauen!")
            self.say(urgent_reply, user_called=True, play_sound=True, loud=True)
            return

        delegate_reply = delegate_mode.handle_speech(self.brain, heard)
        if delegate_reply:
            self.think_aloud("🤖 Jam-Autopilot – ich übernehme alles!")
            self.say(delegate_reply, user_called=True, play_sound=True, loud=True)
            return

        active_sell_reply = active_sell.handle_speech(self.brain, heard)
        if active_sell_reply:
            self.think_aloud("🛒 Jam verkauft aktiv – alle Kanäle!")
            self.say(active_sell_reply, user_called=True, play_sound=True, loud=True)
            return

        promo_reply = shop_heavy_promo.handle_speech(self.brain, heard)
        if promo_reply:
            self.think_aloud("📣 Werbe-Blitz!")
            if getattr(self.brain.state, "jam_shop_heavy_promo", False):
                self._schedule_shop_heavy_promo()
            self.say(promo_reply, user_called=True, play_sound=True, loud=True)
            return

        freelancer_reply = freelancer_connect.handle_speech(self.brain, heard)
        if freelancer_reply:
            self.think_aloud("💼 Freelancer – Skills & Profil!")
            self.say(freelancer_reply, user_called=True, play_sound=True, loud=True)
            return

        fiverr_reply = fiverr_connect.handle_speech(self.brain, heard)
        if fiverr_reply:
            if getattr(self.brain.state, "last_action", "") in ("fiverr_connect", "fiverr_gig"):
                self._present_fiverr_connect(fiverr_reply)
            else:
                self.say(fiverr_reply, user_called=True, play_sound=True, loud=True)
            return

        sell_reply = sales_engine.handle_speech(self.brain, heard)
        if sell_reply:
            self.say(sell_reply, user_called=True, play_sound=True, loud=True)
            return

        money_reply = money_venture.handle_speech(self.brain, heard)
        if money_reply:
            self.say(money_reply, user_called=True, play_sound=True, loud=True)
            return

        fund_reply = gaming_fund.handle_speech(self.brain, heard)
        if fund_reply:
            self._show_gaming_fund_bubble()
            self.say(fund_reply, user_called=True, play_sound=True, loud=True)
            return

        paypal_reward_reply = paypal_reward.handle_speech(self.brain, heard)
        if paypal_reward_reply:
            speech, tier = paypal_reward_reply
            if tier != "none":
                self._celebrate_paypal_reward(tier)
            self.say(speech, user_called=True, play_sound=True, loud=True)
            return

        donate_reply = paypal_donate.handle_speech(self.brain, heard)
        if donate_reply:
            self._present_paypal_donate(donate_reply)
            return

        biz_reply = cursor_business.handle_speech(self.brain, heard)
        if biz_reply:
            self._show_cursor_business_bubble(biz_reply)
            self.say(biz_reply, user_called=True, play_sound=True, loud=True)
            return

        tiktok_reply = tiktok.handle_tiktok_speech(self.brain, heard)
        if tiktok_reply:
            if getattr(self.brain.state, "last_action", "") == "tiktok_connect":
                self._present_tiktok_connect(tiktok_reply)
            else:
                self._present_on_tiktok(tiktok_reply)
            return

        studio_reply = ki_web_studio.handle_speech(self.brain, heard)
        if studio_reply == "__david_pitch__":
            self._say_david_programming_loud()
            return
        if studio_reply:
            self._present_ki_web_studio(studio_reply)
            return

        close_id = game_launcher.parse_close_from_speech(heard_l)
        if close_id:
            self.say(game_launcher.close_game(self.brain, close_id, user_asked=True))
            return

        apex_layout = apex_focus.handle_speech(self.brain, heard)
        if apex_layout:
            self.think_aloud("🎯 Apex vorne – Web hinten!")
            self.say(apex_layout, user_called=True, play_sound=True, loud=True)
            return

        aim_reply = apex_coach.handle_aim_speech(self.brain, heard)
        if aim_reply:
            self.think_aloud("💭 Apex… kein Cheat, nur Coach!")
            self.say(aim_reply, user_called=True, play_sound=True, loud=True)
            return

        game_id = game_launcher.parse_game_from_speech(heard_l)
        if game_id:
            self.think_aloud(f"💭 Starte {game_id.title()}…")
            msg = game_launcher.launch_game(self.brain, game_id)
            self.say(msg, user_called=True, play_sound=True)
            return

        if any(
            p in heard_l
            for p in (
                "was denkst",
                "denkst du",
                "dein gedanke",
                "deine gedanken",
                "was du denkst",
                "sag was du denkst",
                "sag deine gedanken",
                "gedanken sagen",
                "was denkst du",
            )
        ):
            self._tell_what_i_think()
            return
        if "oma" in heard_l or "opi" in heard_l or "großmutter" in heard_l or "grossmutter" in heard_l:
            from jam.speech import gaming_oma_line

            self.think_aloud("💭 Oma-Roast…")
            self.say(gaming_oma_line(self.brain.state), loud=True, play_sound=True)
            return
        if any(
            w in heard_l
            for w in (
                "cheat",
                "cheatest",
                "cheatet",
                "cheaten",
                "gecheat",
                "gecheatet",
                "hacker",
                "hackst",
                "wallhack",
                "aimbot",
                "schummel",
                "schummeln",
                "mogst",
                "mogeln",
            )
        ):
            self._say_gaming_roast(on_start=False, accuse_cheat=True)
            return
        if any(
            w in heard_l
            for w in ("scheisse", "scheiße", "schlecht", "mist", "kacke", "müll", "muell")
        ) and any(
            w in heard_l
            for w in ("spiel", "spiele", "zock", "zockst", "game", "gaming", "runde")
        ):
            self._say_gaming_roast(on_start=False)
            return
        if "sag" in heard_l and "cheat" in heard_l:
            self._say_gaming_roast(on_start=False, accuse_cheat=True)
            return

        music_reply = music.handle_music_speech(self.brain, heard)
        if music_reply == "__dance__":
            self._start_dance()
            self._say_music(music.pick_sing_line(self.brain, heard))
            return
        if music_reply:
            self._start_dance(announce=False)
            self._say_music(music_reply)
            return
        if "fütter" in heard_l or "futter" in heard_l or "essen" in heard_l:
            self._do_feed()
        elif "cursor" in heard and any(
            x in heard for x in ("projekt", "lern", "neu", "probier", "öffne", "oeffne", "code")
        ):
            self._do_cursor_learn()
        elif "letztes cursor" in heard or "cursor wieder" in heard:
            ok, msg = cursor_lab.reopen_last(self.brain)
            self.say(msg)
        elif any(
            w in heard_l
            for w in (
                "weiterentwickel",
                "selbst programm",
                "selbstprogramm",
                "programmier dich",
                "verbesser dich",
            )
        ) and "cursor" in heard_l:
            self._do_cursor_self_improve()
        elif any(w in heard_l for w in ("sätze", "saetze", "satz")) and any(
            w in heard_l for w in ("lern", "internet", "bild", "bau", "form")
        ):
            self._do_learn_sentences()
        elif any(
            x in heard
            for x in (
                "lern",
                "internet",
                "website",
                "webseite",
                "wikipedia",
                "wiktionary",
                "wissen",
            )
        ):
            if "cursor" in heard:
                if cursor_business.hears_cursor_business(heard):
                    self._do_cursor_business(user_initiated=True)
                elif any(w in heard_l for w in ("weiter", "selbst", "verbesser", "code")):
                    self._do_cursor_self_improve()
                else:
                    self._do_cursor_learn()
            else:
                self._learn_web()
        elif any(v in heard_l for v in ("schließ", "schliess", "schließe", "beende", "close")):
            result = commands.execute_phrase(self.brain, heard.replace("jam", "").strip())
            self.say(result)
            return
        elif any(
            v in heard_l
            for v in (
                "öffne",
                "oeffne",
                "starte",
                "start ",
                "starten",
                "launch",
                "spiel ",
                "spiele ",
            )
        ):
            result = commands.execute_phrase(self.brain, heard.replace("jam", "").strip())
            self.say(result)
            return
        if ("spiel" in heard_l or "play" in heard_l) and not game_launcher.parse_game_name(
            heard_l
        ):
            self._do_play()
            return

        smart = ai_optional.smart_reply(self.brain, heard)
        if smart:
            self.say(smart)
        else:
            self.say(greet_on_name(self.brain.state), user_called=True)

    def _do_feed(self) -> None:
        msg = self.brain.state.feed()
        learn = learner.teach_snippet_only(self.brain)
        self._resize_to_scale()
        self.say(msg)
        if learn and self.brain.state.stage >= 1:
            QTimer.singleShot(2500, lambda: self.say(learn or ""))

    def _do_play(self) -> None:
        msg = self.brain.state.play()
        self.vx = random.choice([-2, 2])
        self.vy = random.choice([-2, 2])
        self.say(msg)

    def _apex_foreground_web(self) -> None:
        self.think_aloud("🎯 Apex vorne – Internet im Hintergrund!")
        msg = apex_focus.setup_apex_foreground_browser_background(self.brain)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _apex_aim_tips(self) -> None:
        self.think_aloud("💭 Apex Coach – fair bleiben!")
        self.say(
            apex_coach.pick_coach_reply(self.brain.state),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _launch_fortnite_menu(self) -> None:
        self.think_aloud("💭 Fortnite…")
        self.say(
            game_launcher.launch_game(self.brain, "fortnite"),
            user_called=True,
            play_sound=True,
        )

    def _menu(self, pos) -> None:
        menu = QMenu(self)
        menu.addAction("Alle Aufgaben STOPP ⏸", self._do_stop_all_tasks)
        menu.addAction("TikTok Studio @socialmedia.mentor 📱", self._do_tiktok_studio)
        menu.addAction("TikTok: Stock laden + Remix + Upload 🎬", self._do_tiktok_remix)
        menu.addAction("TikTok 3× Post (Morgen/Mittag/Abend) 📱", self._do_tiktok_daily_posts)
        menu.addAction("X posten (eingeloggt) 🐦", self._do_x_post)
        menu.addAction("Füttern 🍪", self._do_feed)
        menu.addAction("Spielen 🎮", self._do_play)
        menu.addAction("Fortnite starten", self._launch_fortnite_menu)
        menu.addAction("Apex Vordergrund + Internet hinten 🎯🌐", self._apex_foreground_web)
        menu.addAction("Apex Aim-Tipps (kein Cheat) 🎯", self._apex_aim_tips)
        menu.addAction("KI Web Studio präsentieren 🌐", self._present_ki_web_studio_menu)
        menu.addAction("Gratis bestellen (0 € Versand) 📦", self._free_order_menu)
        menu.addAction("Laut: David programmiert alles 🔊", self._say_david_programming_loud)
        menu.addAction("Reichweite aufbauen 📡🌍", self._do_reach_build)
        menu.addAction("Torchwood + Chat mitlesen 💬👁", self._open_torchwood_mod)
        menu.addAction("PC: Alles lernen AN/AUS 🧠", self._toggle_pc_activity_learn)
        menu.addAction("PC: Volle Kontrolle AN/AUS 🖥", self._toggle_pc_control)
        menu.addAction("Jam Browser öffnen 🌐", self._do_open_jam_browser)
        menu.addAction("Jam Browser: Jam übernimmt (Anmeldung) 🤖", self._do_jam_browser_autopilot)
        menu.addAction("Jam-Edge / Login (wie Browser) 🌐", self._do_open_jam_edge)
        menu.addAction("Jam kontaktiert Kunden (volle Erlaubnis) 📨🤖", self._do_full_contact)
        menu.addAction("Welt: Kunden ansprechen (erst Zusage) 🌍📨", self._do_outbound_sales)
        menu.addAction("E-Mail: Jam schreibt & sendet (Gmail) 📧", self._do_auto_email)
        menu.addAction("Dringend: Angebot erstellen ⚡", self._do_urgent_offer)
        menu.addAction("Kunde will es → programmieren 👨‍💻", self._do_urgent_build)
        menu.addAction("Fertig → jetzt verkaufen 🛒", self._do_urgent_sell)
        menu.addAction("Jam übernimmt alles (Autopilot) 🤖", self._do_delegate_autopilot)
        menu.addAction("Verdienst-Liste zeigen 💰📋", self._show_earn_list)
        menu.addAction("Mining-Verdienst als PDF 📄⛏", self._show_mining_pdf_desktop)
        menu.addAction("Verdienst als PDF auf Desktop 📄", self._show_earn_pdf_desktop)
        menu.addAction("Stream heute 19 Uhr – jetzt vorbereiten 🎬", self._do_stream_prep)
        menu.addAction("PC Tag & Nacht an (Geld verdienen) 🌙", self._toggle_night_earn)
        menu.addAction("Wie geht es dir? Jam kümmert sich 💜", self._do_care_check)
        menu.addAction("Spiel-Freund suchen 🎮👥", self._do_find_play_friend)
        menu.addAction("Gaming-Geld verdienen 🎮💰", self._do_gaming_fund)
        menu.addAction("Weltweit verdienen (PayPal global) 🌍💰", self._do_world_earn)
        menu.addAction("Sprint bis 6.6.: Spenden + Verkauf ⚡💰", self._do_deadline_sprint)
        menu.addAction("ALLES für Geld (volle Power) 💰🔥", self._do_money_all_in)
        menu.addAction("Max-Verdienen (alle Kanäle + Cent) 💰", self._do_max_earn)
        menu.addAction("Freelancer: Skills einrichten 💼", self._do_freelancer_setup)
        menu.addAction("Webseite erstellen (modern) 🌐", self._website_menu)
        menu.addAction("Discord einrichten + Bot 💬", self._discord_menu)
        menu.addAction("Fiverr verbinden 💼", self._fiverr_connect_menu)
        menu.addAction("Fiverr verkaufen + PayPal 💼💰", self._fiverr_sell_menu)
        menu.addAction("Fiverr: Gig einstellen 📋", self._fiverr_gig_menu)
        menu.addAction("Fiverr automatisch (Browser) 🤖", self._fiverr_auto_menu)
        menu.addAction("Fiverr+Gumroad: Jam veröffentlicht 🚀", self._do_auto_publish)
        menu.addAction("Plattformen: anmelden & verkaufen 🌐", self._do_platform_signup_sell)
        menu.addAction("Mit Anmeldedaten: überall registrieren 🔐", self._do_register_all_platforms)
        menu.addAction("Firefox: CAPTCHA & E-Mail 🦊", self._do_firefox_captcha)
        menu.addAction("Telegram: Verkaufskanal einrichten 📢", self._do_telegram_setup)
        menu.addAction("Telegram: Werbung posten 📣", self._do_telegram_ads)
        menu.addAction("Klick-Seite + Werbung (AdSense) 📺💰", self._do_ad_earn)
        menu.addAction("VIEL Werbung Jam-Seite 📣🔥", self._do_heavy_shop_promo)
        menu.addAction("Jami Dating 💕", self._do_dating_site)
        menu.addAction("Jam-Seite: Shop + Dashboard 🤖", self._do_master_shop)
        menu.addAction("Jam-Seite: Was verkauft? 📊", self._do_jam_seite_stats)
        menu.addAction("USB-Mining: auf PC starten ⛏️", self._do_pc_mining_start)
        menu.addAction("USB-Mining: stoppen ⏹", self._do_pc_mining_stop)
        menu.addAction("USB-Mining: Status", self._do_pc_mining_status)
        menu.addAction("Jam: Bitcoin-Wallet erstellen ₿", self._do_jam_btc_wallet)
        menu.addAction("GPU-Mining ohne NiceHash (90%) 🎮", self._do_gpu_mining)
        menu.addAction("GPU-Mining TURBO 110% 🎮", self._do_gpu_mining_turbo)
        menu.addAction("GPU-Mining mit NiceHash 🎮", self._do_gpu_mining_nicehash)
        menu.addAction("Mining komplett einrichten ⛏️", self._do_mining_full_setup)
        menu.addAction("Jam-Seite weltweit online 🌍", self._do_shop_worldwide)
        menu.addAction("Eingeloggt – jetzt verkaufen 🔓", self._do_logged_in_sell)
        menu.addAction("Preis verhandeln 🤝", self._do_negotiate_price)
        menu.addAction("Aktiv verkaufen (alle Kanäle) 🛒", self._do_active_sell)
        menu.addAction("Projekt bauen & verkaufen 🛒", self._do_jam_sell)
        menu.addAction("Alle Kanäle: Download + Upload 🌐", self._do_full_ops)
        menu.addAction("Auto-Download ohne Fragen AN/AUS", self._toggle_auto_fetch)
        menu.addAction("PayPal Geld da – Jam belohnen 🎁", self._do_paypal_reward)
        menu.addAction("PayPal-Spende sammeln 💳", self._paypal_donate_menu)
        menu.addAction("Cursor: Produkt + Käufer 🧑‍💻", self._do_cursor_business)
        menu.addAction("Umfragen: teilnehmen & Geld 💰📋", self._do_surveys)
        menu.addAction("Geld-Idee umsetzen 💰", self._do_money_venture)
        menu.addAction("Internet: lernen was Jam will 🌐", self._do_internet_learn_free)
        menu.addAction("Sätze aus Internet lernen 📖", self._do_learn_sentences)
        menu.addAction("Internet-Lernen (neue Website) 📚", self._learn_web)
        menu.addAction("Cursor: Jam weiterentwickeln 🔧", self._do_cursor_self_improve)
        menu.addAction("Cursor: Neues Projekt 🧪", self._do_cursor_learn)
        menu.addAction("Cursor: Letztes öffnen", lambda: self.say(cursor_lab.reopen_last(self.brain)[1]))
        menu.addSeparator()
        menu.addAction("TikTok: @socialmedia.mentor verbinden 🔗", self._tiktok_connect_menu)
        menu.addAction("TikTok: Vorstellung 📱", self._tiktok_friends)
        menu.addAction("TikTok: 100+ Likes ♥", self._tiktok_hearts_menu)
        menu.addAction("YouTube Musik öffnen 🎵", self._open_youtube)
        menu.addAction("YouTube Lied wählen… 🎵", self._pick_youtube_song)
        menu.addAction("Nächstes YouTube-Lied ⏭", self._next_youtube_song)
        menu.addAction("Zur Musik tanzen 💃", self._start_dance_and_speak)
        menu.addAction("Feuerwerk 🎆", self._do_fireworks)
        menu.addAction("Show-Animation ▶", self._do_idle_show_menu)
        menu.addSeparator()
        menu.addAction("Loben 👍", self._do_praise)
        menu.addAction("Tadeln 👎", self._do_scold)
        menu.addAction("Regel lehren…", self._teach_rule)
        menu.addAction("Gedanken sagen 💭", self._tell_what_i_think)
        menu.addAction("Gedanken anzeigen…", self._show_thought)
        menu.addSeparator()
        menu.addAction("Befehl lehren…", self._teach_cmd)
        menu.addAction("Befehl ausführen…", self._run_cmd)
        menu.addAction("Ins Zimmer (bis „komm raus“)", self._menu_go_room)
        menu.addAction("Kurz verstecken (kommt zurück)", self._do_hide)
        menu.addAction("Frech sein 😈", self._start_mischief)
        menu.addAction("Furzen 💨", self._do_fart)
        menu.addAction("Speichern & Wachstum", self._show_stats)
        menu.addAction("Test: Sprechen 🔊", self._test_speak)
        menu.addAction("Alexa-Modus: Hilfe 🎙", self._alexa_help_menu)
        menu.addAction("Beenden (nur wenn Immer-an AUS)", self._try_quit)
        menu.exec(self.mapToGlobal(pos))

    def closeEvent(self, event) -> None:  # noqa: N802
        if always_on.should_block_quit(self.brain.state):
            event.ignore()
            self._ensure_visible()
            self.say(
                always_on.block_quit_line(self.brain.state),
                user_called=False,
                play_sound=True,
            )
            return
        super().closeEvent(event)

    def _try_quit(self) -> None:
        if always_on.should_block_quit(self.brain.state):
            self.say(always_on.block_quit_line(self.brain.state), user_called=True)
            self._ensure_visible()
            return
        QApplication.quit()

    def _show_ki_studio_bubble(self) -> None:
        self.bubble_label.setStyleSheet(self._bubble_studio_style)
        self.bubble_label.setMaximumWidth(320)
        self.bubble_label.setText(ki_web_studio.format_passion_bubble(self.brain.state))
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(16_000)

    def _say_david_programming_loud(self) -> None:
        self.think_aloud("💭 Laut: David programmiert alles!")
        self.bubble_label.setStyleSheet(self._bubble_studio_style)
        self.bubble_label.setMaximumWidth(320)
        self.bubble_label.setText(
            ki_web_studio.format_david_pitch_bubble(self.brain.state)
        )
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(12_000)
        line = ki_web_studio.say_david_pitch_loud(self.brain, open_site=True)
        self.say(line, user_called=True, play_sound=True, loud=True)

    def _present_ki_web_studio(self, speech: str) -> None:
        if speech.startswith("KI Web Studio hab") or speech.startswith("KI Web Studio lässt"):
            self.say(speech, user_called=True)
            return
        self.think_aloud("💭 KI Web Studio – ich bin SO begeistert!")
        self._show_ki_studio_bubble()
        self._start_dance()
        self.say(speech, user_called=True, play_sound=True, loud=True)
        QTimer.singleShot(12_000, self._stop_dance)

    def _present_ki_web_studio_menu(self) -> None:
        self._present_ki_web_studio(
            ki_web_studio.present_site(self.brain, user_asked=True)
        )

    def _show_order_bubble(self, text: str) -> None:
        from jam.owner import owner_name

        log = getattr(self.brain.state, "free_orders_log", []) or []
        if log and isinstance(log[-1], dict):
            entry = log[-1]
            bubble = free_orders.format_order_bubble(
                owner_name(self.brain.state),
                {"name": entry.get("name", "?"), "detail": entry.get("detail", "")},
                entry.get("order_no", "?"),
            )
        else:
            bubble = text[:220]
        self.bubble_label.setStyleSheet(self._bubble_order_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(bubble)
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(14_000)

    def _present_free_order(self, speech: str) -> None:
        if speech.startswith("Browser klappt") or "gerade erst" in speech:
            self.say(speech, user_called=True)
            return
        self.think_aloud("💭 Gratis-Shopping für dich!")
        self._show_order_bubble(speech)
        self.say(speech, user_called=True, play_sound=True, loud=True)

    def _free_order_menu(self) -> None:
        self._present_free_order(free_orders.place_free_order(self.brain, user_asked=True))

    def _present_paypal_donate(self, speech: str) -> None:
        self.think_aloud("💭 PayPal-Spenden für David!")
        self.bubble_label.setStyleSheet(self._bubble_paypal_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(paypal_donate.format_donate_bubble(self.brain.state))
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(16_000)
        self.say(speech, user_called=True, play_sound=True)

    def _paypal_donate_menu(self) -> None:
        self._present_paypal_donate(
            paypal_donate.collect_donations(self.brain, user_asked=True)
        )

    def _present_tiktok_connect(self, speech: str) -> None:
        if speech.startswith("TikTok hab") or speech.startswith("TikTok-Verbindung"):
            self.say(speech, user_called=True)
            return
        acc = tiktok_account.get_linked_account(self.brain.state)
        bridge = tiktok_account.bridge_status_text()
        self.think_aloud(f"💭 TikTok @{acc} verbunden!")
        self.bubble_label.setStyleSheet(self._bubble_tiktok_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(
            tiktok_account.format_connect_bubble(acc, bridge=bridge)
        )
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(12_000)
        self.say(speech, user_called=True, play_sound=True)

    def _tiktok_connect_menu(self) -> None:
        msg = tiktok_account.connect_account(
            self.brain,
            account=tiktok_account.DEFAULT_ACCOUNT,
            user_asked=True,
        )
        self._present_tiktok_connect(msg)

    def _present_fiverr_connect(self, speech: str) -> None:
        u = fiverr_connect.get_username(self.brain.state)
        self.think_aloud("💭 Fiverr Seller verbunden!")
        self.bubble_label.setStyleSheet(self._bubble_paypal_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(fiverr_connect.format_connect_bubble(u))
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(12_000)
        self.say(speech, user_called=True, play_sound=True)

    def _do_freelancer_setup(self) -> None:
        self.think_aloud("💼 Freelancer Skills – loslegen!")
        msg = freelancer_connect.run_freelancer_setup(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _website_menu(self) -> None:
        self.think_aloud("💭 Moderne Webseite anbieten!")
        msg = website_control.handle_speech(self.brain, "webseite erstellen lassen modern")
        self.say(msg or "Webseiten-Angebot.", user_called=True, play_sound=True, loud=True)

    def _discord_menu(self) -> None:
        self.think_aloud("💭 Discord – Bot & Skripte einrichten!")
        msg = discord_control.handle_speech(self.brain, "discord einrichten")
        self.say(msg or "Discord-Ordner angelegt.", user_called=True, play_sound=True, loud=True)

    def _fiverr_connect_menu(self) -> None:
        msg = fiverr_connect.connect_fiverr(
            self.brain, username=fiverr_connect.DEFAULT_USERNAME, user_asked=True
        )
        self._present_fiverr_connect(msg)

    def _fiverr_sell_menu(self) -> None:
        self.think_aloud("💭 Fiverr verkaufen – PayPal für David!")
        msg = fiverr_connect.run_fiverr_sell_mission(self.brain, user_initiated=True)
        self._present_fiverr_connect(msg)

    def _fiverr_gig_menu(self) -> None:
        self.think_aloud("💭 Fiverr Gig – Text in Zwischenablage!")
        msg = fiverr_connect.push_latest_gig(self.brain, user_initiated=True)
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _do_auto_publish(self) -> None:
        self.think_aloud("🚀 Jam veröffentlicht auf Fiverr & Gumroad!")
        from jam.auto_publish import run_auto_publish_all

        self.say(
            run_auto_publish_all(self.brain, user_initiated=True),
            user_called=True,
            play_sound=True,
            loud=True,
        )

    def _fiverr_auto_menu(self) -> None:
        self.think_aloud("🤖 Fiverr-Assistent – Browser + Einfügen!")
        msg = fiverr_assist.run_fiverr_assist(
            self.brain, mode="full", user_initiated=True, try_publish=True
        )
        self.say(msg, user_called=True, play_sound=True, loud=True)

    def _present_on_tiktok(self, intro: str) -> None:
        if intro.startswith("TikTok klappt") or intro.startswith("TikTok hab"):
            self.say(intro, user_called=True)
            return
        self.think_aloud("💭 TikTok… ich stell mich vor!")
        self._show_tiktok_intro(intro)
        self.say(intro, user_called=True, play_sound=True)

    def _tiktok_friends(self) -> None:
        self._present_on_tiktok(tiktok.present_on_tiktok(self.brain, user_asked=True))

    def _launch_tiktok_hearts(self) -> None:
        if self._tiktok_hearts_show is not None:
            try:
                self._tiktok_hearts_show.close()
            except RuntimeError:
                pass
            self._tiktok_hearts_show = None
        show = tiktok_hearts.launch_hearts_show(self)
        if show is not None:
            self._tiktok_hearts_show = show
            show.destroyed.connect(self._on_tiktok_hearts_closed)

    def _present_tiktok_hearts(self) -> None:
        speech = tiktok_hearts.celebrate_hundred_likes(self.brain, user_asked=True)
        if speech.startswith("TikTok hab") or speech.startswith("TikTok-Herzen"):
            self.say(speech, user_called=True)
            return
        self.think_aloud("💭 TikTok… Herzen für 100+ Likes!")
        self._launch_tiktok_hearts()
        self.bubble_label.setStyleSheet(self._bubble_tiktok_style)
        self.bubble_label.setMaximumWidth(300)
        self.bubble_label.setText(tiktok_hearts.format_hearts_bubble())
        self.bubble_label.adjustSize()
        self._layout_with_bubble()
        self.bubble_label.show()
        self.bubble_timer.start(14_000)
        self._start_dance()
        self.say(speech, user_called=True, play_sound=True, loud=True)
        QTimer.singleShot(10_000, self._stop_dance)

    def _tiktok_hearts_menu(self) -> None:
        self._present_tiktok_hearts()

    def _open_youtube(self) -> None:
        self._start_dance(announce=False)
        self._say_music(music.open_youtube_music(self.brain, user_pick=True))

    def _pick_youtube_song(self) -> None:
        text, ok = QInputDialog.getText(
            self,
            "YouTube Lied",
            f"Song, Künstler oder Stil (z. B. Lofi, Believer):\n\nJam kennt: {music.list_song_picks()}",
        )
        if ok and text.strip():
            self._start_dance(announce=False)
            self._say_music(
                music.open_youtube_music(self.brain, query=text.strip(), user_pick=True)
            )

    def _next_youtube_song(self) -> None:
        from jam.music import YouTubeRequest, open_youtube_request

        self._start_dance(announce=False)
        self._say_music(open_youtube_request(self.brain, YouTubeRequest(kind="next")))

    def _start_dance_and_speak(self) -> None:
        self._start_dance()

    def _alexa_help_menu(self) -> None:
        reply = voice_assistant.route_or_none(self.brain, "was kannst du") or ""
        if reply:
            self.say(reply, user_called=True, play_sound=True)

    def _test_speak(self) -> None:
        self.say(
            "Hallo! Ich bin Jam – leises Monster-Geräusch!",
            loud=False,
            play_sound=True,
        )

    def _do_praise(self) -> None:
        note, ok = QInputDialog.getText(
            self, "Loben", "Wofür lobst du Jam? (optional)", text=""
        )
        if ok:
            self.say(upbringing.praise(self.brain.state, note if note else ""))
            self.brain.save()

    def _do_scold(self) -> None:
        note, ok = QInputDialog.getText(
            self, "Tadeln", "Was war falsch? (optional)", text=""
        )
        if ok:
            self.say(upbringing.scold(self.brain.state, note if note else ""))
            self.brain.save()

    def _teach_rule(self) -> None:
        trig, ok1 = QInputDialog.getText(
            self, "Regel", "Wenn… (z.B. „ich arbeite“ oder „du spielst“):"
        )
        if not ok1 or not trig:
            return
        beh, ok2 = QInputDialog.getText(
            self, "Regel", "…dann soll Jam… (z.B. „sei leise“):", text="sei leise"
        )
        if ok2 and beh:
            self.say(upbringing.teach_rule(self.brain.state, trig, beh))
            self.brain.save()

    def _show_thought(self) -> None:
        t = self.brain.state.last_thought or "Ich hab noch nichts gedacht."
        if self.brain.state.stage >= 1 and t and "nichts gedacht" not in t:
            self.speak_thought(t)
        traits = ", ".join(
            f"{k} {v:.0%}" for k, v in self.brain.state.traits.items()
        )
        QMessageBox.information(
            self,
            "Jams Kopf",
            f"Letzter Gedanke:\n{t}\n\nLetzte Aktion: {self.brain.state.last_action}\n"
            f"Persönlichkeit: {traits}\n"
            f"Regeln: {len(self.brain.state.rules)}",
        )

    def _learn_web(self) -> None:
        self._do_internet_learn_free(user_initiated=True)

    def _teach_cmd(self) -> None:
        phrase, ok1 = QInputDialog.getText(self, "Jam merken", "Wenn ich sage (Phrase):")
        if not ok1 or not phrase:
            return
        action, ok2 = QInputDialog.getText(
            self, "Jam merken", "…dann ausführen (Befehl/URL):", text="notepad"
        )
        if ok2 and action:
            self.brain.state.learn_command(phrase.lower().replace("jam", "").strip(), action)
            self.brain.save()
            self._resize_to_scale()
            self.say(f"Gemerkt! Bei '{phrase}' mache ich: {action}")

    def _run_cmd(self) -> None:
        phrase, ok = QInputDialog.getText(self, "Befehl", "Was soll Jam tun?")
        if ok and phrase:
            result = commands.execute_phrase(self.brain, phrase)
            self.say(result)

    def _show_stats(self) -> None:
        s = self.brain.state
        upbringing.ensure_traits(s)
        traits = "\n".join(f"  {k}: {v:.0%}" for k, v in s.traits.items())
        QMessageBox.information(
            self,
            "Jam Status",
            f"Besitzer: {s.owner_name}\n"
            f"Stufe: {s.stage}\nXP: {s.xp}\nGröße: {s.size_scale:.2f}\n"
            f"Hunger: {s.hunger:.0%}\nGlück: {s.happiness:.0%}\n"
            f"Disziplin: {s.discipline:.0%}\n"
            f"Alter: {s.age_days:.1f} Tage\n"
            f"Erziehung: {s.praise_count}× Lob, {s.scold_count}× Tadel\n"
            f"Persönlichkeit:\n{traits}\n"
            f"Regeln: {len(s.rules)}\n"
            f"Lern-Websites: {getattr(s, 'learn_opens', 0)} (zuletzt: {getattr(s, 'last_learn_site', '–')})\n"
            f"Cursor-Experimente: {len(s.cursor_sessions)} (gesamt {s.cursor_opens_total})\n"
            f"Gelernte Wörter: {len(s.learned_words)}\n"
            f"Gelernte Sätze: {len(getattr(s, 'learned_sentences', []) or [])} "
            f"(+{getattr(s, 'sentence_learn_count', 0)} aus Netz)\n"
            f"{internet_learn.stats_line(s)}\n"
            f"{earn_report.stats_line(s)}\n"
            f"{max_earn.stats_line(s)}\n"
            f"{world_earn.stats_line(s)}\n"
            f"{extreme_learn.stats_line(s)}\n"
            f"{stream_prep.stats_line(s)}\n"
            f"{keep_awake.stats_line(s)}\n"
            f"{night_focus.stats_line(s)}\n"
            f"Fürsorge: {getattr(s, 'jam_care_checks', 0)} Checks, "
            f"Bond {getattr(s, 'owner_bond', 0.5):.0%}, "
            f"Spiel-Freunde: {getattr(s, 'friend_match_count', 0)}\n"
            f"{auto_ops.stats_line(s)}\n"
            f"{sales_engine.stats_line(s)}\n"
            f"{money_venture.stats_line(s)}\n"
            f"PayPal-Spenden: {getattr(s, 'paypal_donate_opens', 0)}× "
            f"({paypal_donate.PAYPAL_EMAIL})\n"
            f"{paypal_reward.stats_line(s)}\n"
            f"Cursor-Business: {getattr(s, 'cursor_business_count', 0)} Produkte\n"
            f"PC-Lernen: {'AN' if getattr(s, 'jam_pc_activity_learn', True) else 'AUS'} "
            f"({pc_activity_learn.summary_for_owner(s)})\n"
            f"{torchwood_moderator.stats_line(s)}\n"
            f"PC-Kontrolle: {'AN' if getattr(s, 'pc_control_enabled', False) else 'AUS'} "
            f"({getattr(s, 'pc_control_actions', 0)} Aktionen)\n"
            f"Gaming-Kasse: {getattr(s, 'gaming_fund_raised_eur', 0):.0f} / "
            f"{getattr(s, 'gaming_fund_goal_eur', 60):.0f} € (PayPal)\n"
            f"Cursor Selbst-Updates: {getattr(s, 'cursor_self_improve_count', 0)}\n"
            f"TikTok: @{getattr(s, 'tiktok_account', '–')} "
            f"({'verbunden' if getattr(s, 'tiktok_connected', False) else 'nicht verbunden'})\n"
            f"{fiverr_connect.stats_line(s)}\n"
            f"{delegate_mode.status_line(s)}\n"
            f"{urgent_offers.stats_line(s)}\n"
            f"3D-Look: {getattr(s, 'visual_theme', 'base')} "
            f"(Stufe {getattr(s, 'visual_evolution_step', 0)}, "
            f"Tiefe +{getattr(s, 'appearance_depth_boost', 0):.2f})\n"
            f"Befehle: {len(s.learned_commands)}",
        )

    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.MouseButton.LeftButton:
            self.drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
            self.brain.state.add_xp(1)
            self.brain.state.last_interaction = time.time()
        elif event.button() == Qt.MouseButton.RightButton:
            self._menu(event.pos())

    def mouseMoveEvent(self, event) -> None:
        if self.drag_pos and event.buttons() & Qt.MouseButton.LeftButton:
            self.move(event.globalPosition().toPoint() - self.drag_pos)

    def mouseReleaseEvent(self, event) -> None:
        self.drag_pos = None

    def mouseDoubleClickEvent(self, event) -> None:
        self.say(greet_on_name(self.brain.state))
        if self._gaming:
            self._say_gaming_roast(on_start=False)
