support scroll for hover
This commit is contained in:
@@ -81,6 +81,12 @@ class Front:
|
||||
self._sidebar_filter_avatar_id: Optional[str] = None
|
||||
self._sidebar_filter_open: bool = False
|
||||
|
||||
# hover 轮换状态(滚轮切换)
|
||||
self._hover_anchor_pos: Optional[tuple[int, int]] = None
|
||||
self._hover_candidates: List[str] = [] # avatar_id 列表(当前锚点下)
|
||||
self._hover_index: int = 0
|
||||
self._hover_last_build_ms: int = 0
|
||||
|
||||
def add_events(self, new_events: List[Event]):
|
||||
self.events.extend(new_events)
|
||||
if len(self.events) > 1000:
|
||||
@@ -114,6 +120,14 @@ class Front:
|
||||
current_step_task = asyncio.create_task(self._step_once_async())
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
self._handle_mouse_click()
|
||||
# 兼容旧版滚轮为 MOUSEBUTTON 4/5
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button in (4, 5):
|
||||
delta = 1 if event.button == 4 else -1
|
||||
self._on_mouse_wheel(delta)
|
||||
# pygame 2 的标准滚轮事件
|
||||
elif getattr(pygame, "MOUSEWHEEL", None) is not None and event.type == pygame.MOUSEWHEEL:
|
||||
# event.y: 上滚为正,下滚为负
|
||||
self._on_mouse_wheel(int(getattr(event, "y", 0)))
|
||||
if self._auto_step and self._last_step_ms >= self.step_interval_ms:
|
||||
if current_step_task is None or current_step_task.done():
|
||||
current_step_task = asyncio.create_task(self._step_once_async())
|
||||
@@ -155,7 +169,7 @@ class Front:
|
||||
STATUS_BAR_HEIGHT,
|
||||
)
|
||||
self._assign_avatar_images()
|
||||
hovered_avatar = draw_avatars_and_pick_hover(
|
||||
hovered_default, hover_candidates = draw_avatars_and_pick_hover(
|
||||
pygame,
|
||||
self.screen,
|
||||
self.colors,
|
||||
@@ -166,6 +180,7 @@ class Front:
|
||||
self._get_display_center,
|
||||
STATUS_BAR_HEIGHT,
|
||||
)
|
||||
hovered_avatar = self._pick_hover_with_scroll(hovered_default, hover_candidates)
|
||||
# 先绘制状态栏和侧边栏,再绘制 tooltip 保证 tooltip 在最上层
|
||||
draw_status_bar(pygame, self.screen, self.colors, self.status_font, self.margin, self.world, self._auto_step)
|
||||
|
||||
@@ -197,6 +212,18 @@ class Front:
|
||||
self._sidebar_ui = sidebar_ui
|
||||
if hovered_avatar is not None:
|
||||
draw_tooltip_for_avatar(pygame, self.screen, self.colors, self.tooltip_font, hovered_avatar)
|
||||
# 绘制候选徽标(仅当存在多个候选)
|
||||
if len(hover_candidates) >= 2:
|
||||
from .rendering import draw_hover_badge
|
||||
# 取当前 hover 对象的显示中心
|
||||
cx_f, cy_f = self._get_display_center(hovered_avatar, self.tile_size, self.margin)
|
||||
cx, cy = int(cx_f), int(cy_f)
|
||||
# 计算当前索引(1-based)
|
||||
try:
|
||||
idx = self._hover_candidates.index(hovered_avatar.id)
|
||||
except ValueError:
|
||||
idx = 0
|
||||
draw_hover_badge(pygame, self.screen, self.colors, self.tooltip_font, cx, cy, idx + 1, len(hover_candidates), STATUS_BAR_HEIGHT)
|
||||
elif hovered_region is not None:
|
||||
mouse_x, mouse_y = pygame.mouse.get_pos()
|
||||
draw_tooltip_for_region(pygame, self.screen, self.colors, self.tooltip_font, hovered_region, mouse_x, mouse_y)
|
||||
@@ -222,6 +249,64 @@ class Front:
|
||||
def _get_region_font(self, size: int):
|
||||
return _get_region_font_cached(self.pygame, self._region_font_cache, size, self.font_path)
|
||||
|
||||
# --- Hover 轮换逻辑 ---
|
||||
def _is_mouse_near_anchor(self, radius_px: int = 20) -> bool:
|
||||
if self._hover_anchor_pos is None:
|
||||
return False
|
||||
mx, my = self.pygame.mouse.get_pos()
|
||||
ax, ay = self._hover_anchor_pos
|
||||
dx = mx - ax
|
||||
dy = my - ay
|
||||
return (dx * dx + dy * dy) <= (radius_px * radius_px)
|
||||
|
||||
def _rebuild_hover_candidates(self, hovered_default: Optional[Avatar], candidates: List[Avatar]) -> None:
|
||||
self._hover_anchor_pos = self.pygame.mouse.get_pos()
|
||||
self._hover_candidates = [a.id for a in candidates]
|
||||
if hovered_default is not None and hovered_default.id in self._hover_candidates:
|
||||
self._hover_index = self._hover_candidates.index(hovered_default.id)
|
||||
else:
|
||||
self._hover_index = 0
|
||||
self._hover_last_build_ms = self._now_ms()
|
||||
|
||||
def _pick_hover_with_scroll(self, hovered_default: Optional[Avatar], candidates: List[Avatar]) -> Optional[Avatar]:
|
||||
# 无候选时清空状态
|
||||
if not candidates:
|
||||
self._hover_anchor_pos = None
|
||||
self._hover_candidates = []
|
||||
self._hover_index = 0
|
||||
return None
|
||||
# 当前候选ID列表
|
||||
current_ids = [a.id for a in candidates]
|
||||
# 需要重建的情形:
|
||||
# 1) 没有锚点;2) 鼠标离锚点太远;3) 候选集合变化;4) 距上次构建时间过久
|
||||
need_rebuild = False
|
||||
if self._hover_anchor_pos is None:
|
||||
need_rebuild = True
|
||||
elif not self._is_mouse_near_anchor():
|
||||
need_rebuild = True
|
||||
elif current_ids != self._hover_candidates:
|
||||
need_rebuild = True
|
||||
elif (self._now_ms() - self._hover_last_build_ms) > 800:
|
||||
need_rebuild = True
|
||||
if need_rebuild:
|
||||
self._rebuild_hover_candidates(hovered_default, candidates)
|
||||
# 选出当前下标对应的 avatar
|
||||
if not self._hover_candidates:
|
||||
return hovered_default
|
||||
self._hover_index %= max(1, len(self._hover_candidates))
|
||||
aid = self._hover_candidates[self._hover_index]
|
||||
return self.world.avatar_manager.avatars.get(aid, hovered_default)
|
||||
|
||||
def _on_mouse_wheel(self, delta: int) -> None:
|
||||
# 仅当有至少两个候选且鼠标仍在锚点附近时进行轮换
|
||||
if len(self._hover_candidates) >= 2 and self._is_mouse_near_anchor():
|
||||
if delta > 0:
|
||||
self._hover_index = (self._hover_index - 1) % len(self._hover_candidates)
|
||||
elif delta < 0:
|
||||
self._hover_index = (self._hover_index + 1) % len(self._hover_candidates)
|
||||
# 轻微刷新锚点时间,避免过快过期
|
||||
self._hover_last_build_ms = self._now_ms()
|
||||
|
||||
def _assign_avatar_images(self):
|
||||
import random
|
||||
for avatar_id, avatar in self.world.avatar_manager.avatars.items():
|
||||
|
||||
@@ -280,10 +280,9 @@ def draw_avatars_and_pick_hover(
|
||||
margin: int,
|
||||
get_display_center: Optional[Callable[[Avatar, int, int], Tuple[float, float]]] = None,
|
||||
top_offset: int = 0,
|
||||
) -> Optional[Avatar]:
|
||||
) -> Tuple[Optional[Avatar], List[Avatar]]:
|
||||
mouse_x, mouse_y = pygame_mod.mouse.get_pos()
|
||||
hovered = None
|
||||
min_dist = float("inf")
|
||||
candidates_with_dist: List[Tuple[float, Avatar]] = []
|
||||
for avatar_id, avatar in simulator.world.avatar_manager.avatars.items():
|
||||
if get_display_center is not None:
|
||||
cx_f, cy_f = get_display_center(avatar, tile_size, margin)
|
||||
@@ -298,16 +297,18 @@ def draw_avatars_and_pick_hover(
|
||||
image_y = cy - image_rect.height // 2
|
||||
screen.blit(avatar_image, (image_x, image_y))
|
||||
if image_rect.collidepoint(mouse_x - image_x, mouse_y - image_y):
|
||||
hovered = avatar
|
||||
min_dist = 0
|
||||
dist = math.hypot(mouse_x - cx, mouse_y - cy)
|
||||
candidates_with_dist.append((dist, avatar))
|
||||
else:
|
||||
radius = max(8, tile_size // 3)
|
||||
pygame_mod.draw.circle(screen, colors["avatar"], (cx, cy), radius)
|
||||
dist = math.hypot(mouse_x - cx, mouse_y - cy)
|
||||
if dist <= radius and dist < min_dist:
|
||||
hovered = avatar
|
||||
min_dist = dist
|
||||
return hovered
|
||||
if dist <= radius:
|
||||
candidates_with_dist.append((dist, avatar))
|
||||
candidates_with_dist.sort(key=lambda t: t[0])
|
||||
hovered = candidates_with_dist[0][1] if candidates_with_dist else None
|
||||
candidate_avatars: List[Avatar] = [a for _, a in candidates_with_dist]
|
||||
return hovered, candidate_avatars
|
||||
|
||||
|
||||
def draw_tooltip(pygame_mod, screen, colors, lines: List[str], mouse_x: int, mouse_y: int, font, min_width: Optional[int] = None):
|
||||
@@ -412,3 +413,28 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def draw_hover_badge(pygame_mod, screen, colors, font, center_x: int, center_y: int, index: int, total: int, top_offset: int = 0):
|
||||
"""
|
||||
在给定中心附近绘制一个小徽标,显示 index/total(索引从1开始)。
|
||||
徽标默认放在头像上方偏右位置。
|
||||
"""
|
||||
label = f"{index}/{total}"
|
||||
surf = font.render(label, True, colors["text"])
|
||||
pad_x = 6
|
||||
pad_y = 2
|
||||
w = surf.get_width() + pad_x * 2
|
||||
h = surf.get_height() + pad_y * 2
|
||||
# 徽标位置:头像中心的右上角
|
||||
x = int(center_x + 10)
|
||||
y = int(center_y + top_offset - 24 - h)
|
||||
rect = pygame_mod.Rect(x, y, w, h)
|
||||
# 半透明背景与描边
|
||||
bg = pygame_mod.Surface((w, h), pygame_mod.SRCALPHA)
|
||||
bg.fill((20, 20, 20, 180))
|
||||
screen.blit(bg, (rect.x, rect.y))
|
||||
pygame_mod.draw.rect(screen, colors.get("tooltip_bd", (90, 90, 90)), rect, 1, border_radius=6)
|
||||
# 文本
|
||||
screen.blit(surf, (rect.x + pad_x, rect.y + pad_y))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -29,4 +29,4 @@ id,name,exclusion_ids,desc,weight,condition
|
||||
27,腼腆,26,你对待和他人结为道侣或者双修比较谨慎,1,
|
||||
28,舔狗,13;14;22;27,你对异性中外貌出众者格外友善,倾向主动接近、帮助与合作。,1,
|
||||
29,嫉妒,11;23,你对在修为、外貌或财富等方面远超于你的人容易产生敌意,更倾向对其冷淡、挑衅或打压。,1,
|
||||
30,穿越者,,你来自现代社会,怀念现代社会的一切,希望调查清楚你来的原因,早日回到现代,你的思考方式都是现代化的,1,
|
||||
30,穿越者,,你来自现代社会,怀念现代社会的一切,希望调查清楚你来的原因,早日回到现代,你的思考方式都是现代化的,100,
|
||||
|
Can't render this file because it contains an unexpected character in line 26 and column 121.
|
Reference in New Issue
Block a user