local M = {} local DEFAULT_INTERVAL_S = 5.0 local MANUAL_PAUSE_S = 30.0 local MAX_DOTS = 7 local ARROW_W = 32 local DOT_R = 4 local DOT_GAP = 6 local COLOR_ARROW_BG = 0x00000080 local COLOR_ARROW_FG = 0xE0E0E0FF local COLOR_DOT = 0xC0C0C060 local COLOR_DOT_ACTIVE = 0xFFFFFFFF local COLOR_OVERLAY = 0xE0E0E0FF local COLOR_FALLBACK_BG = 0x303030FF function M.new_state(module_id, teaser_paths, interval) return { module_id = module_id, teaser_paths = teaser_paths or {}, textures = {}, -- index -> texture or nil if load-failed loaded = {}, -- index -> true once loaded index = 1, timer = 0, interval = interval or DEFAULT_INTERVAL_S, manual_pause = 0, left_rect = nil, right_rect = nil, } end local function ensure_texture(s, i, default_texture) if s.loaded[i] then return s.textures[i] end s.loaded[i] = true local path = s.teaser_paths[i] if not path then s.textures[i] = default_texture; return default_texture end local tex, err = engine.module.load_texture(s.module_id, path) s.textures[i] = tex or default_texture return s.textures[i] end function M.update(s, dt) if #s.teaser_paths < 2 then return end if s.manual_pause > 0 then s.manual_pause = s.manual_pause - dt return end s.timer = s.timer + dt if s.timer >= s.interval then s.timer = 0 s.index = s.index + 1 if s.index > #s.teaser_paths then s.index = 1 end end end function M.render(s, x, y, w, h, default_texture) -- Background (clipping is up to the parent panel — we just clip-render). engine.render.draw_rect(x, y, w, h, COLOR_FALLBACK_BG) local total = math.max(1, #s.teaser_paths) local tex = ensure_texture(s, s.index, default_texture) or default_texture if tex then engine.render.draw_texture(tex, x, y) end if total > 1 then -- Arrows. s.left_rect = { x = x, y = y + h / 2 - 16, w = ARROW_W, h = 32 } s.right_rect = { x = x + w - ARROW_W, y = y + h / 2 - 16, w = ARROW_W, h = 32 } engine.render.draw_rect(s.left_rect.x, s.left_rect.y, ARROW_W, 32, COLOR_ARROW_BG) engine.render.draw_rect(s.right_rect.x, s.right_rect.y, ARROW_W, 32, COLOR_ARROW_BG) engine.render.draw_text("<", s.left_rect.x + 10, s.left_rect.y + 6, 20, COLOR_ARROW_FG) engine.render.draw_text(">", s.right_rect.x + 10, s.right_rect.y + 6, 20, COLOR_ARROW_FG) -- Dots. local dot_count = math.min(total, MAX_DOTS) local dots_w = dot_count * (DOT_R * 2) + (dot_count - 1) * DOT_GAP local dot_x = x + (w - dots_w) / 2 local dot_y = y + h - 16 for i = 1, dot_count do local col = (i == s.index) and COLOR_DOT_ACTIVE or COLOR_DOT engine.render.draw_rect(dot_x, dot_y, DOT_R * 2, DOT_R * 2, col) dot_x = dot_x + DOT_R * 2 + DOT_GAP end if total > MAX_DOTS then local label = string.format("%d / %d", s.index, total) engine.render.draw_text(label, x + w - 80, dot_y - 4, 14, COLOR_OVERLAY) end end end function M.handle_click(s, mx, my) if not s or not s.left_rect then return false end if engine.spatial.aabb_contains_point(s.left_rect, mx, my) then s.index = s.index - 1 if s.index < 1 then s.index = #s.teaser_paths end s.timer = 0 s.manual_pause = MANUAL_PAUSE_S return true end if engine.spatial.aabb_contains_point(s.right_rect, mx, my) then s.index = s.index + 1 if s.index > #s.teaser_paths then s.index = 1 end s.timer = 0 s.manual_pause = MANUAL_PAUSE_S return true end return false end return M