@yyyu-star/arcade-kit 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,10 @@
1
- /**
2
- * 牌桌互动组件(0.11.0,仓库根 docs/table-fun.md 第 0 节):头像菜单、互动面板、表情菜单、首次提示、冷却和护盾、连击和回敬、
3
- * 电脑的反应、名场面横幅。几个牌桌游戏共用一套,外观、位置规则、动画、声音完全一样;游戏只做接线:
4
- * 告诉组件每个座位的头像在屏幕上的矩形、名字、是不是电脑和互动性格,哪些地方不能盖(手牌、操作按钮),怎么把东西发出去,
5
- * 再把收到的(好友房经房间转发的,单机自己的)交给 receive、把公开的名场面交给 moment。
6
- *
7
- * - DOM 实现:头像上盖一块透明的热区接点按和长按(画布游戏把世界坐标换成屏幕矩形传进来就行),菜单和面板是普通的 DOM,点按区 ≥ 44px;
8
- * 效果(表情、道具、污渍)走 fun.ts 的图层,点击穿透。这个文件只在调用时碰 DOM,import 时不碰(房间、测试里 import 没事)。
9
- * - 动画只动 transform、opacity;量位置(getBoundingClientRect)只在开菜单、给冒烟报控件时。
10
- * - 公平:人人一样、免费;只是画面和声音,不停计时、不改规则;电脑的出牌不读这些。电脑的反应只在画面层(单机)或房间的互动钩子里算。
11
- * - kit 不认识任何游戏:短语表、名场面的映射、电脑人物都由游戏给。
12
- */
13
- import { type ChatBlockWhy } from './chat.js';
1
+ import { checkPrefsKey, type ChatResultLike } from './table-chat.js';
14
2
  import { type KitTheme } from './theme.js';
15
3
  import { type FunLayer, type FunPhase, type PropId, type RectLike } from './fun.js';
16
4
  import { type BotReactConfig, type FunEvent, type FunMood, type FunTemper } from './fun-logic.js';
17
5
  export * from './fun-logic.js';
18
- export type { RectLike };
6
+ export type { RectLike, ChatResultLike };
7
+ export { checkPrefsKey };
19
8
  /** 一个座位此刻的样子(每次用到时现问,座位换人、头像挪了都跟得上) */
20
9
  export interface FunSeat {
21
10
  /** 头像的屏幕矩形(CSS 像素,getBoundingClientRect 的结果能直接传) */
@@ -27,6 +16,8 @@ export interface FunSeat {
27
16
  bot?: boolean;
28
17
  /** 电脑的互动性格,默认 normal */
29
18
  temper?: FunTemper;
19
+ /** 名牌、筹码数这些挂在头像旁的东西(屏幕矩形,0.18.2):气泡避开它们,不盖住名字和钱数 */
20
+ plate?: RectLike;
30
21
  }
31
22
  /**
32
23
  * storageKey 的检查(0.13.1):它就是游戏 id,kit 拿它拼 `<id>.hint.fun`、`<id>.fun.blocked`。同站的几个游戏和平台只靠 `<id>.` 前缀分开存储,
@@ -47,6 +38,14 @@ export interface TableFunOptions {
47
38
  avoid?(): readonly RectLike[];
48
39
  /** "互动"按钮放哪(屏幕矩形,组件画按钮);null 就不画(比如没坐下) */
49
40
  button?(): RectLike | null;
41
+ /**
42
+ * "互动"按钮的样子(0.18.2,替掉游戏自己对 .atf-btn 的 CSS 覆盖):scale 把字(13px)和图标(22px)一起放大(舞台放大了的大屏用,最小 1);
43
+ * row 为 true 时图标和字横排。每次 refresh 现问
44
+ */
45
+ buttonStyle?(): {
46
+ scale?: number;
47
+ row?: boolean;
48
+ };
50
49
  /** 发出去:好友房走 kit emote,单机直接回给 receive */
51
50
  send(id: number, to?: number): void;
52
51
  /** 现在能不能发(好友房:坐下了才能),默认能 */
@@ -63,6 +62,25 @@ export interface TableFunOptions {
63
62
  chat?(text: string): ChatResultLike;
64
63
  /** 设置里"聊天:开 / 不看"(和"互动"分开):不看时别人说的不冒气泡、不进记录,自己也没有聊天页;默认开 */
65
64
  chatEnabled?(): boolean;
65
+ /**
66
+ * 聊天的设置(弹幕开关、电脑版聊天框收没收起)存在哪个 localStorage key(0.19.0):必须是 `<storageKey>.<名字>`,比如 'texas.chat';
67
+ * 默认 `<storageKey>.chat`,游戏不用传
68
+ */
69
+ prefsKey?: string;
70
+ /** 省内存模式(0.19.0):开着时弹幕默认关(用户自己打开了照样飘)。不给就看 navigator.deviceMemory ≤ 2 */
71
+ lite?(): boolean;
72
+ /** 电脑版聊天框放哪(0.19.0,用 left、top、width):不给就在牌桌四个角里挑一个不压手牌、按钮、名牌的 */
73
+ chatDock?(): RectLike | null;
74
+ /**
75
+ * 弹幕在哪个区域飘(0.19.0):不给就是屏幕 8%–58%,上沿压到最上面一排座位的头像、名牌下面;轨道自动避开手牌、按钮、名牌、头像、center
76
+ */
77
+ danmakuArea?(): RectLike | null;
78
+ /** 手机版"聊天"按钮放哪(0.19.0,屏幕矩形):不给就挨着"互动"按钮,先右后左再上下挑不压东西的一侧;挤的牌桌(手机横屏)最好给 */
79
+ chatButton?(): RectLike | null;
80
+ /**
81
+ * 牌桌中间的信息区(0.19.0):剩余张数、底池、牌墩、出牌区、弃牌区这些,弹幕不横穿、聊天框和气泡不压。可以给几块
82
+ */
83
+ center?(): RectLike[];
66
84
  /** 设置里"互动:开 / 不看",关了别人的一律不播,单机电脑也不反应;默认开 */
67
85
  enabled?(): boolean;
68
86
  /** 屏蔽名单按这个分开(房间码;单机一个固定值),默认 'solo' */
@@ -88,17 +106,9 @@ export interface TableFunOptions {
88
106
  now?(): number;
89
107
  }
90
108
  /** RoomClient.chat 的结果里组件要看的部分(不 import client.ts,单机的游戏也能用组件) */
91
- export type ChatResultLike = {
92
- ok: true;
93
- } | {
94
- ok: false;
95
- code: string;
96
- why?: ChatBlockWhy;
97
- waitMs?: number;
98
- };
99
- /** 聊天:气泡停多久、页面里记最近几条(只在内存里) */
109
+ /** 聊天:气泡停多久(0.19.0 起 5 秒)、页面里记最近几条(只在内存里) */
100
110
  export declare const CHAT_UI: {
101
- readonly bubbleMs: 4000;
111
+ readonly bubbleMs: 5000;
102
112
  readonly keep: 30;
103
113
  };
104
114
  /**
@@ -107,6 +117,13 @@ export declare const CHAT_UI: {
107
117
  */
108
118
  export declare function hintSeatFor(seats: readonly (FunSeat | null)[], pick?: () => number | null): number | null;
109
119
  type Tab = 'phrase' | 'sticker' | 'prop' | 'chat';
120
+ /**
121
+ * "互动" / "聊天"按钮的尺寸参数(0.18.2):游戏没给 buttonStyle 就不写行内样式(不然会盖掉还没迁过来的游戏自己对 .atf-btn 的 CSS)
122
+ */
123
+ export declare function styleButton(b: HTMLElement, st: {
124
+ scale?: number;
125
+ row?: boolean;
126
+ } | undefined): void;
110
127
  export declare class TableFun {
111
128
  readonly layer: FunLayer;
112
129
  private readonly ui;
@@ -124,12 +141,13 @@ export declare class TableFun {
124
141
  private lastProp;
125
142
  private lastSticker;
126
143
  private timers;
127
- /** 聊天记录(最近 CHAT_UI.keep 条,只在内存里;关页面、换房间就没了) */
128
- private chatLog;
144
+ /** 聊天框、输入条、弹幕、记录(0.19.0,table-chat.ts) */
145
+ private readonly cui;
129
146
  private chatLogEl;
130
147
  /** 每个人最近的那个聊天气泡:同一个人连说两条,后一条顶掉前一条 */
131
148
  private says;
132
- private unread;
149
+ /** 正在显示的气泡:新气泡也避开它们,不叠在一起 */
150
+ private live;
133
151
  /** 键盘弹出时跟着可见区挪面板(输入框聚焦时挂上) */
134
152
  private vvFix;
135
153
  private dead;
@@ -144,6 +162,12 @@ export declare class TableFun {
144
162
  private enabled;
145
163
  /** 头像挪了、窗口变了、座位换人:重新摆热区和按钮(游戏重排画面后调;窗口 resize 自己会调) */
146
164
  refresh(): void;
165
+ /** 聊天框、弹幕、气泡不能盖的地方:游戏的禁区、"互动"按钮、各家头像和名牌 */
166
+ private blocked;
167
+ /** 弹幕开着吗(游戏设置里显示用,0.19.0) */
168
+ danmaku(): boolean;
169
+ /** 游戏设置里改弹幕开关(和聊天框、面板里的"弹幕"按钮同一份设置) */
170
+ setDanmaku(on: boolean): void;
147
171
  /** 头像热区:点一下开菜单(别人)或表情菜单(自己);按住 longPressMs 直接扔上次的道具 / 发上次的表情 */
148
172
  private makeHot;
149
173
  private tapSeat;
@@ -164,18 +188,14 @@ export declare class TableFun {
164
188
  openPanel(tab?: Tab): void;
165
189
  closeAll(): void;
166
190
  private chatOn;
167
- /** 聊天页:上面最近 30 条(新的在下),下面一行输入框 + 发送,再下面一行小字(还能打几个字、为什么没发出去) */
191
+ /** 聊天页:上面最近 30 条(新的在下,名字按座位配色),弹幕开关,下面一行输入框 + 发送,再下面一行小字(还能打几个字、为什么没发出去) */
168
192
  private chatBody;
169
- /** 被限速时"发送"按钮倒计时,到点恢复 */
170
- private countdown;
171
- private renderLog;
172
193
  /**
173
194
  * iOS 键盘弹出时会盖住下半屏、页面不一定跟着滚:按 visualViewport 把面板整个往上挪到键盘上面,记录区限在可见高度的 60% 以内。
174
195
  * 只动这个面板的 top(transform 里是 placeMenu 摆的位置),收起菜单时摘掉
175
196
  */
176
197
  private hookViewport;
177
198
  private unhookViewport;
178
- private setUnread;
179
199
  /**
180
200
  * 有人说了一句(好友房 RoomClient 的 onChat 交进来):头像旁冒 4 秒气泡、记进聊天页。
181
201
  * 别人说的在"聊天:不看"、被屏蔽(单个或全部屏蔽)时一律不显示;面板没开在聊天页时"互动"按钮上亮个小红点
@@ -200,7 +220,10 @@ export declare class TableFun {
200
220
  /** 电脑的反应一定看得见:表情加一句话的气泡同时出;回敬再让道具飞过去 */
201
221
  private perform;
202
222
  private line;
203
- /** 头像旁的一句话:朝屏幕中间那侧冒,2.4 秒后淡出;cls 加样式(聊天的气泡能折行) */
223
+ /**
224
+ * 头像旁的一句话:2.4 秒后淡出(聊天 5 秒)。字号跟着屏幕(0.19.0:电脑 18–22px、手机 ≥ 15px),最多两行折行;
225
+ * 位置挑不压"互动"按钮、聊天框、各家名牌筹码和别人头像的一侧
226
+ */
204
227
  bubble(seat: number, text: string, ms?: number, cls?: string): HTMLElement | undefined;
205
228
  /** 头像上方冒一个字(×N、回敬!、护盾):放大弹一下再飘走 */
206
229
  private flash;
@@ -1 +1 @@
1
- {"version":3,"file":"table-fun.d.ts","sourceRoot":"","sources":["../src/table-fun.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"table-fun.d.ts","sourceRoot":"","sources":["../src/table-fun.ts"],"names":[],"mappings":"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"}
package/dist/table-fun.js CHANGED
@@ -10,11 +10,13 @@
10
10
  * - 公平:人人一样、免费;只是画面和声音,不停计时、不改规则;电脑的出牌不读这些。电脑的反应只在画面层(单机)或房间的互动钩子里算。
11
11
  * - kit 不认识任何游戏:短语表、名场面的映射、电脑人物都由游戏给。
12
12
  */
13
- import { CHAT_MAX, chatRejectText } from './chat.js';
13
+ import { chatRejectText } from './chat.js';
14
+ import { CHAT_CSS, ChatUi, checkPrefsKey } from './table-chat.js';
14
15
  import { applyTheme } from './theme.js';
15
16
  import { PROPS, STICKERS, createFunLayer, propSvg, showSticker, stickerSvg, throwAt } from './fun.js';
16
- import { BOT_REACT, FUN_TIMING, FunCooldown, FunTally, botReact, funItem, newBotState, fitMenuWidth, placeMenu, propId, stickerId, stickerMood, } from './fun-logic.js';
17
+ import { BOT_REACT, chatFont, FUN_TIMING, FunCooldown, FunTally, botReact, funItem, newBotState, fitMenuWidth, placeBubble, placeMenu, propId, stickerId, stickerMood, } from './fun-logic.js';
17
18
  export * from './fun-logic.js';
19
+ export { checkPrefsKey };
18
20
  /**
19
21
  * storageKey 的检查(0.13.1):它就是游戏 id,kit 拿它拼 `<id>.hint.fun`、`<id>.fun.blocked`。同站的几个游戏和平台只靠 `<id>.` 前缀分开存储,
20
22
  * 所以只认游戏 id 的写法(小写字母、数字、连字符,字母或数字开头,最多 32 位);带点("guandan." 会拼出 "guandan..fun")、大写、空串、
@@ -35,8 +37,9 @@ const DEFAULT_LINES = {
35
37
  cry: ['服了', '下把再来', '太难了'],
36
38
  };
37
39
  const HINT_TEXT = '点头像扔道具,长按连扔';
38
- /** 聊天:气泡停多久、页面里记最近几条(只在内存里) */
39
- export const CHAT_UI = { bubbleMs: 4000, keep: 30 };
40
+ /** RoomClient.chat 的结果里组件要看的部分(不 import client.ts,单机的游戏也能用组件) */
41
+ /** 聊天:气泡停多久(0.19.0 起 5 秒)、页面里记最近几条(只在内存里) */
42
+ export const CHAT_UI = { bubbleMs: 5000, keep: 30 };
40
43
  /**
41
44
  * 首次提示冒在哪个座位(纯函数,测试直接测):游戏给了 pick 就听它的,它给的座位空着或不存在当作"现在不行";
42
45
  * 没给就挑第一个电脑,没有电脑挑第一个别人。null = 这会儿不提示,过一会儿再问
@@ -68,23 +71,36 @@ const CSS = `
68
71
  .atf-cool{position:absolute;inset:0;border-radius:var(--ak-radius-btn,10px);background:rgba(0,0,0,.5);pointer-events:none}
69
72
  .atf-cool::after{content:'';position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px 0 0 -13px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:var(--ak-title,#f3d98b);animation:atf-spin .7s linear infinite}
70
73
  .atf-btn{position:absolute;left:0;top:0;pointer-events:auto;min-width:44px;min-height:44px;border-radius:var(--ak-radius-btn,22px);border:2px solid var(--ak-border,#d4a64a);background:var(--ak-soft,#5a3a22);color:var(--ak-fg,#fff6e0);font:inherit;font-size:13px;font-weight:700;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;padding:2px 6px;box-sizing:border-box}
71
- .atf-btn svg{width:22px;height:22px}
74
+ .atf-btn svg{width:var(--atf-icon,22px);height:var(--atf-icon,22px)}
72
75
  .atf-btn.atf-dot::after{content:'';position:absolute;right:2px;top:2px;width:10px;height:10px;border-radius:50%;background:#e8323a;border:2px solid var(--ak-fg,#fff6e0)}
73
- .atf-say{white-space:normal;max-width:240px;word-break:break-all;font-weight:600}
76
+ .atf-say{font-weight:600}
74
77
  .atf-log{display:flex;flex-direction:column;gap:4px;font-size:14px;line-height:1.4;min-height:44px}
75
78
  .atf-log b{color:var(--ak-title,#f3d98b);font-weight:700;margin-right:4px}
76
79
  .atf-log .atf-empty{opacity:.6}
77
80
  .atf-input{flex:1;min-width:0;min-height:44px;border:1px solid var(--ak-border,rgba(212,166,74,.6));border-radius:var(--ak-radius-btn,10px);background:#fffaf0;color:#2b1d14;font:inherit;font-size:16px;padding:0 10px;box-sizing:border-box}
78
81
  .atf-note{font-size:13px;margin:4px 4px 0;opacity:.85;min-height:18px}
79
82
  .atf-chip:disabled{opacity:.5;cursor:default}
80
- .atf-bubble{position:absolute;left:0;top:0;max-width:220px;padding:6px 12px;border-radius:var(--ak-radius,14px);background:#fffaf0;color:#2b1d14;font-size:15px;font-weight:700;box-shadow:0 3px 8px rgba(0,0,0,.35);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
83
+ .atf-bubble{position:absolute;left:0;top:0;z-index:2;max-width:240px;padding:.35em .75em;border-radius:var(--ak-radius,14px);background:#fffaf0;color:#2b1d14;font-size:15px;line-height:1.3;font-weight:700;box-shadow:0 3px 8px rgba(0,0,0,.35);white-space:normal;word-break:break-all;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;pointer-events:none}
81
84
  .atf-flash{position:absolute;left:0;top:0;font-size:26px;font-weight:900;color:var(--ak-title,#f3d98b);-webkit-text-stroke:1.5px #2b1d14;pointer-events:none;white-space:nowrap}
82
85
  .atf-banner{position:absolute;left:50%;top:18%;padding:10px 34px;background:linear-gradient(#c8323a,#8a1a20);border:2px solid var(--ak-title,#f3d98b);border-radius:6px;color:var(--ak-fg,#fff6e0);font-size:28px;font-weight:900;letter-spacing:4px;text-align:center;pointer-events:none;box-shadow:0 6px 20px rgba(0,0,0,.4)}
83
86
  .atf-banner small{display:block;font-size:15px;letter-spacing:1px;color:var(--ak-title,#f3d98b)}
84
87
  @keyframes atf-pop{from{opacity:0;scale:.92}to{opacity:1;scale:1}}
85
88
  @keyframes atf-spin{to{transform:rotate(360deg)}}
86
- `;
89
+ ${CHAT_CSS}`;
87
90
  const SMILE = '<svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"><circle cx="32" cy="32" r="24"/><circle cx="23" cy="27" r="2.5" fill="currentColor"/><circle cx="41" cy="27" r="2.5" fill="currentColor"/><path d="M20 37Q32 49 44 37"/></svg>';
91
+ /**
92
+ * "互动" / "聊天"按钮的尺寸参数(0.18.2):游戏没给 buttonStyle 就不写行内样式(不然会盖掉还没迁过来的游戏自己对 .atf-btn 的 CSS)
93
+ */
94
+ export function styleButton(b, st) {
95
+ const k = Math.max(1, st?.scale ?? 1);
96
+ b.style.fontSize = st ? `${13 * k}px` : '';
97
+ b.style.flexDirection = st?.row ? 'row' : '';
98
+ b.style.gap = st?.row ? `${6 * k}px` : '';
99
+ if (st)
100
+ b.style.setProperty('--atf-icon', `${22 * k}px`);
101
+ else
102
+ b.style.removeProperty('--atf-icon');
103
+ }
88
104
  /** 挂子元素(Workers 的类型里也有一个 append,签名对不上,所以统一用 appendChild) */
89
105
  function add(parent, ...kids) {
90
106
  for (const k of kids)
@@ -113,12 +129,13 @@ export class TableFun {
113
129
  lastProp = 'tomato';
114
130
  lastSticker = 'smug';
115
131
  timers = new Set();
116
- /** 聊天记录(最近 CHAT_UI.keep 条,只在内存里;关页面、换房间就没了) */
117
- chatLog = [];
132
+ /** 聊天框、输入条、弹幕、记录(0.19.0,table-chat.ts) */
133
+ cui;
118
134
  chatLogEl = null;
119
135
  /** 每个人最近的那个聊天气泡:同一个人连说两条,后一条顶掉前一条 */
120
136
  says = new Map();
121
- unread = false;
137
+ /** 正在显示的气泡:新气泡也避开它们,不叠在一起 */
138
+ live = new Set();
122
139
  /** 键盘弹出时跟着可见区挪面板(输入框聚焦时挂上) */
123
140
  vvFix = null;
124
141
  dead = false;
@@ -126,6 +143,7 @@ export class TableFun {
126
143
  constructor(opts) {
127
144
  // 先查 key 再碰 DOM:不合格的不留下半个界面
128
145
  checkStorageKey(opts.storageKey);
146
+ const prefsKey = checkPrefsKey(opts.prefsKey ?? `${opts.storageKey}.chat`, opts.storageKey);
129
147
  this.o = opts;
130
148
  this.P = opts.phrases.length;
131
149
  this.doc = document;
@@ -142,6 +160,32 @@ export class TableFun {
142
160
  this.ui.style.zIndex = String(z + 1);
143
161
  applyTheme(this.ui, opts.theme);
144
162
  this.doc.body.appendChild(this.ui);
163
+ this.cui = new ChatUi({
164
+ ui: this.ui,
165
+ fx: this.layer.el,
166
+ prefsKey,
167
+ phrases: opts.phrases,
168
+ phrase: (i) => void this.pick(i),
169
+ send: (t) => this.o.chat(t),
170
+ canSend: () => this.canSend(),
171
+ blocked: () => this.blocked(),
172
+ bubbles: () => [...this.live].map((e) => e.getBoundingClientRect()),
173
+ seatBoxes: () => this.o.seats().flatMap((x) => (x ? [x.rect, ...(x.plate ? [x.plate] : [])] : [])),
174
+ barBlocked: () => {
175
+ const me = this.o.seats().find((x) => !!x?.me);
176
+ return [...(this.o.avoid?.() ?? []), ...(this.btn ? [this.btn.getBoundingClientRect()] : []), ...(me ? [me.rect, ...(me.plate ? [me.plate] : [])] : [])];
177
+ },
178
+ button: () => (this.btn ? this.btn.getBoundingClientRect() : null),
179
+ styleButton: (b) => styleButton(b, this.o.buttonStyle?.()),
180
+ dock: () => this.o.chatDock?.() ?? null,
181
+ chatButton: () => this.o.chatButton?.() ?? null,
182
+ area: () => this.o.danmakuArea?.() ?? null,
183
+ lite: () => this.o.lite?.() ?? (navigator.deviceMemory ?? 8) <= 2,
184
+ history: () => this.openPanel('chat'),
185
+ later: (ms, fn) => this.later(ms, fn),
186
+ now: () => this.now(),
187
+ ...(opts.log ? { log: opts.log } : {}),
188
+ });
145
189
  window.addEventListener('resize', this.onResize);
146
190
  this.refresh();
147
191
  this.later(1500, () => this.hint());
@@ -202,7 +246,6 @@ export class TableFun {
202
246
  b.className = 'atf-btn';
203
247
  b.type = 'button';
204
248
  b.innerHTML = `${SMILE}<span>互动</span>`;
205
- b.classList.toggle('atf-dot', this.unread);
206
249
  b.addEventListener('click', (e) => {
207
250
  e.stopPropagation();
208
251
  if (this.open?.dataset['kind'] === 'panel')
@@ -213,10 +256,34 @@ export class TableFun {
213
256
  this.ui.appendChild(b);
214
257
  this.btn = b;
215
258
  }
259
+ styleButton(this.btn, this.o.buttonStyle?.());
216
260
  this.btn.style.width = `${Math.max(44, r.width)}px`;
217
261
  this.btn.style.height = `${Math.max(44, r.height)}px`;
218
262
  this.btn.style.transform = `translate(${r.left}px,${r.top}px)`;
219
263
  }
264
+ this.cui.refresh(this.chatOn());
265
+ }
266
+ /** 聊天框、弹幕、气泡不能盖的地方:游戏的禁区、"互动"按钮、各家头像和名牌 */
267
+ blocked() {
268
+ const out = [...(this.o.avoid?.() ?? []), ...(this.o.center?.() ?? [])];
269
+ if (this.btn)
270
+ out.push(this.btn.getBoundingClientRect());
271
+ for (const s of this.o.seats()) {
272
+ if (!s)
273
+ continue;
274
+ out.push(s.rect);
275
+ if (s.plate)
276
+ out.push(s.plate);
277
+ }
278
+ return out;
279
+ }
280
+ /** 弹幕开着吗(游戏设置里显示用,0.19.0) */
281
+ danmaku() {
282
+ return this.cui.danmaku();
283
+ }
284
+ /** 游戏设置里改弹幕开关(和聊天框、面板里的"弹幕"按钮同一份设置) */
285
+ setDanmaku(on) {
286
+ this.cui.setDanmaku(on);
220
287
  }
221
288
  /** 头像热区:点一下开菜单(别人)或表情菜单(自己);按住 longPressMs 直接扔上次的道具 / 发上次的表情 */
222
289
  makeHot(i) {
@@ -408,6 +475,7 @@ export class TableFun {
408
475
  openPanel(tab = this.tab) {
409
476
  if (!this.canSend() || !this.btn)
410
477
  return;
478
+ this.cui.closeBar();
411
479
  this.tab = tab;
412
480
  const r = this.btn.getBoundingClientRect();
413
481
  const hasChat = this.chatOn();
@@ -477,6 +545,7 @@ export class TableFun {
477
545
  closeAll() {
478
546
  this.open?.remove();
479
547
  this.open = null;
548
+ this.cui.bindLog(null, this.chatLogEl);
480
549
  this.chatLogEl = null;
481
550
  this.unhookViewport();
482
551
  }
@@ -484,89 +553,23 @@ export class TableFun {
484
553
  chatOn() {
485
554
  return !!this.o.chat && (this.o.chatEnabled?.() ?? true);
486
555
  }
487
- /** 聊天页:上面最近 30 条(新的在下),下面一行输入框 + 发送,再下面一行小字(还能打几个字、为什么没发出去) */
556
+ /** 聊天页:上面最近 30 条(新的在下,名字按座位配色),弹幕开关,下面一行输入框 + 发送,再下面一行小字(还能打几个字、为什么没发出去) */
488
557
  chatBody(b, body) {
489
- this.setUnread(false);
558
+ this.cui.setUnread(false);
490
559
  // 记录自己滚,输入框一直在下面看得见
491
560
  const log = this.el('div', 'atf-log atf-scroll');
492
561
  log.style.maxHeight = '160px';
493
562
  this.chatLogEl = log;
494
- this.renderLog();
495
- add(body, log);
496
- const row = this.el('div', 'atf-row');
563
+ this.cui.bindLog(log);
564
+ const dm = this.cui.chip(this.cui.danmakuLabel(), 'fun:danmaku', () => this.cui.setDanmaku(!this.cui.danmaku()), 'atf-soft');
565
+ dm.style.marginTop = '6px';
566
+ const { row, note, input } = this.cui.inputRow('fun');
497
567
  row.style.marginTop = '8px';
498
- row.style.flexWrap = 'nowrap';
499
- const input = this.el('input', 'atf-input');
500
- input.type = 'text';
501
- input.enterKeyHint = 'send';
502
- input.autocomplete = 'off';
503
- input.placeholder = '说点什么';
504
- input.dataset['key'] = 'fun:chatInput';
505
- const send = this.chip('发送', 'fun:chatSend', () => go());
506
- const note = this.el('div', 'atf-note');
507
- const left = () => {
508
- const n = CHAT_MAX - Array.from(input.value.trim()).length;
509
- note.textContent = n < 0 ? `多了 ${-n} 个字` : `还能打 ${n} 个字`;
510
- send.disabled = n < 0 || send.dataset['wait'] === '1';
511
- };
512
- const go = () => {
513
- if (!input.value.trim() || send.disabled)
514
- return;
515
- const r = this.o.chat(input.value);
516
- if (r.ok) {
517
- input.value = '';
518
- left();
519
- return;
520
- }
521
- note.textContent = chatRejectText(r.code, r.why);
522
- if (r.code === 'chat_too_fast' && r.waitMs)
523
- this.countdown(send, r.waitMs, left);
524
- };
525
- input.addEventListener('input', left);
526
- input.addEventListener('keydown', (e) => {
527
- e.stopPropagation();
528
- if (e.key === 'Enter' && !e.isComposing)
529
- go();
530
- });
531
568
  input.addEventListener('focus', () => this.hookViewport(b));
532
- add(row, input, send);
533
- add(body, row, note);
534
- left();
569
+ add(body, log, dm, row, note);
535
570
  body.style.maxHeight = '';
536
571
  log.scrollTop = log.scrollHeight;
537
572
  }
538
- /** 被限速时"发送"按钮倒计时,到点恢复 */
539
- countdown(btn, ms, done) {
540
- btn.dataset['wait'] = '1';
541
- btn.disabled = true;
542
- const end = this.now() + ms;
543
- const tick = () => {
544
- const s = Math.ceil((end - this.now()) / 1000);
545
- if (s <= 0 || !btn.isConnected) {
546
- btn.textContent = '发送';
547
- delete btn.dataset['wait'];
548
- done();
549
- return;
550
- }
551
- btn.textContent = `${s} 秒`;
552
- this.later(Math.min(1000, end - this.now()), tick);
553
- };
554
- tick();
555
- }
556
- renderLog() {
557
- const log = this.chatLogEl;
558
- if (!log)
559
- return;
560
- log.replaceChildren();
561
- if (this.chatLog.length === 0)
562
- add(log, this.el('div', 'atf-empty', '还没人说话。这里的话不保存,房间散了就没了'));
563
- for (const m of this.chatLog) {
564
- const row = this.el('div', '');
565
- add(row, this.el('b', '', `${m.name}:`), this.doc.createTextNode(m.text));
566
- add(log, row);
567
- }
568
- log.scrollTop = log.scrollHeight;
569
- }
570
573
  /**
571
574
  * iOS 键盘弹出时会盖住下半屏、页面不一定跟着滚:按 visualViewport 把面板整个往上挪到键盘上面,记录区限在可见高度的 60% 以内。
572
575
  * 只动这个面板的 top(transform 里是 placeMenu 摆的位置),收起菜单时摘掉
@@ -598,10 +601,6 @@ export class TableFun {
598
601
  }
599
602
  this.vvFix = null;
600
603
  }
601
- setUnread(on) {
602
- this.unread = on;
603
- this.btn?.classList.toggle('atf-dot', on);
604
- }
605
604
  /**
606
605
  * 有人说了一句(好友房 RoomClient 的 onChat 交进来):头像旁冒 4 秒气泡、记进聊天页。
607
606
  * 别人说的在"聊天:不看"、被屏蔽(单个或全部屏蔽)时一律不显示;面板没开在聊天页时"互动"按钮上亮个小红点
@@ -614,28 +613,24 @@ export class TableFun {
614
613
  return;
615
614
  if (!src.me && (!(this.o.chatEnabled?.() ?? true) || this.isBlocked(from)))
616
615
  return;
617
- this.chatLog.push({ seat: from, name: src.me ? '我' : src.name, text });
618
- while (this.chatLog.length > CHAT_UI.keep)
619
- this.chatLog.shift();
620
- this.says.get(from)?.remove();
616
+ const old = this.says.get(from);
617
+ if (old) {
618
+ this.live.delete(old);
619
+ old.remove();
620
+ }
621
621
  const bb = this.bubble(from, text, CHAT_UI.bubbleMs, 'atf-say');
622
622
  if (bb)
623
623
  this.says.set(from, bb);
624
- if (this.chatLogEl)
625
- this.renderLog();
626
- else if (!src.me)
627
- this.setUnread(true);
624
+ this.cui.push({ seat: from, name: src.me ? '我' : src.name, text, me: !!src.me }, !!this.chatLogEl);
628
625
  this.o.log?.(`聊天 ${from}`);
629
626
  }
630
627
  /** 房间拒了一条(RoomClient 的 onChatError):聊天页开着就在小字那行说一声 */
631
628
  chatError(code) {
632
- const note = this.open?.querySelector('.atf-note');
633
- if (note)
634
- note.textContent = chatRejectText(code);
629
+ this.cui.note(chatRejectText(code));
635
630
  }
636
631
  /** 有菜单或面板开着(游戏可以据此暂停自己的快捷键) */
637
632
  isOpen() {
638
- return this.open !== null;
633
+ return this.open !== null || this.cui.typing();
639
634
  }
640
635
  // ───────────── 屏蔽(只是自己不看;这一局 / 这个房间有效) ─────────────
641
636
  blockKey() {
@@ -693,7 +688,11 @@ export class TableFun {
693
688
  return;
694
689
  if (it.kind === 'phrase') {
695
690
  const t = to !== undefined && to !== from ? this.seat(to) : null;
696
- this.bubble(from, `${t ? `@${t.name} ` : ''}${this.o.phrases[it.index] ?? ''}`);
691
+ const say = `${t ? `@${t.name} ` : ''}${this.o.phrases[it.index] ?? ''}`;
692
+ this.bubble(from, say);
693
+ // 短语也是说话:有聊天的时候进记录、飘弹幕(0.19.0)
694
+ if (this.chatOn())
695
+ this.cui.push({ seat: from, name: src.me ? '我' : src.name, text: say, me: !!src.me }, !!this.chatLogEl);
697
696
  this.o.log?.(`短语 ${from}:${it.index}`);
698
697
  }
699
698
  else if (it.kind === 'sticker') {
@@ -770,20 +769,45 @@ export class TableFun {
770
769
  return lines[Math.floor(this.rand() * lines.length) % lines.length];
771
770
  }
772
771
  // ───────────── 小效果 ─────────────
773
- /** 头像旁的一句话:朝屏幕中间那侧冒,2.4 秒后淡出;cls 加样式(聊天的气泡能折行) */
772
+ /**
773
+ * 头像旁的一句话:2.4 秒后淡出(聊天 5 秒)。字号跟着屏幕(0.19.0:电脑 18–22px、手机 ≥ 15px),最多两行折行;
774
+ * 位置挑不压"互动"按钮、聊天框、各家名牌筹码和别人头像的一侧
775
+ */
774
776
  bubble(seat, text, ms = 2400, cls = '') {
775
777
  const s = this.seat(seat);
776
778
  if (!s)
777
779
  return undefined;
778
780
  const b = this.el('div', `atf-bubble ${cls}`.trim(), text);
781
+ const view = { width: window.innerWidth, height: window.innerHeight };
782
+ const fs = chatFont(view);
783
+ // 一行约 16 个字:30 个字的聊天两行放得下
784
+ const maxW = Math.min(fs * 16 + 24, view.width - 16);
785
+ b.style.fontSize = `${fs}px`;
786
+ b.style.maxWidth = `${maxW}px`;
779
787
  this.layer.el.appendChild(b);
780
- const w = Math.min(cls ? 240 : 220, b.offsetWidth || text.length * 15 + 24);
781
- const cx = s.rect.left + s.rect.width / 2;
782
- const right = cx < window.innerWidth / 2;
783
- const x = Math.max(4, Math.min(window.innerWidth - w - 4, right ? s.rect.left + s.rect.width + 6 : s.rect.left - 6 - w));
784
- const y = Math.max(4, s.rect.top - 6);
785
- b.style.transform = `translate(${x}px,${y}px)`;
786
- void anim(b, [{ opacity: 0 }, { opacity: 1 }], 160).then(() => anim(b, [{ opacity: 1 }, { opacity: 0 }], 260, ms)).then(() => b.remove());
788
+ const w = Math.min(maxW, b.offsetWidth || text.length * fs + 24);
789
+ const h = b.offsetHeight || fs * 1.6;
790
+ // 避开"互动"按钮、各家的名牌筹码和别人的头像(0.18.2):原来总往朝中心那侧放,竖屏自己的气泡被按钮压住半截
791
+ const avoid = [...(this.o.avoid?.() ?? []), ...(this.o.center?.() ?? []), ...this.cui.rects(), ...[...this.live].map((e) => e.getBoundingClientRect())];
792
+ if (this.btn)
793
+ avoid.push(this.btn.getBoundingClientRect());
794
+ this.o.seats().forEach((o, i) => {
795
+ if (!o)
796
+ return;
797
+ if (o.plate)
798
+ avoid.push(o.plate);
799
+ if (i !== seat)
800
+ avoid.push(o.rect);
801
+ });
802
+ const at = placeBubble(s.rect, w, h, { width: window.innerWidth, height: window.innerHeight }, avoid);
803
+ b.style.transform = `translate(${at.left}px,${at.top}px)`;
804
+ this.live.add(b);
805
+ void anim(b, [{ opacity: 0 }, { opacity: 1 }], 160)
806
+ .then(() => anim(b, [{ opacity: 1 }, { opacity: 0 }], 260, ms))
807
+ .then(() => {
808
+ this.live.delete(b);
809
+ b.remove();
810
+ });
787
811
  return b;
788
812
  }
789
813
  /** 头像上方冒一个字(×N、回敬!、护盾):放大弹一下再飘走 */
@@ -857,12 +881,14 @@ export class TableFun {
857
881
  if (h.style.display !== 'none')
858
882
  out[`fun:seat${i}`] = c(h);
859
883
  });
860
- this.open?.querySelectorAll('[data-key]').forEach((e) => (out[e.dataset['key']] = c(e)));
884
+ // 菜单、面板、聊天框、手机的"聊天"按钮和输入条(data-key 在 .atf-ui 里唯一)
885
+ this.ui.querySelectorAll('[data-key]').forEach((e) => (out[e.dataset['key']] = c(e)));
861
886
  return out;
862
887
  }
863
888
  destroy() {
864
889
  this.dead = true;
865
890
  this.unhookViewport();
891
+ this.cui.destroy();
866
892
  for (const t of this.timers)
867
893
  clearTimeout(t);
868
894
  this.timers.clear();