@yyyu-star/arcade-kit 0.16.0 → 0.18.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.
- package/CHANGELOG.md +19 -0
- package/README.md +20 -1
- package/dist/chat-words.d.ts +7 -0
- package/dist/chat-words.d.ts.map +1 -0
- package/dist/chat-words.js +15 -0
- package/dist/chat-words.js.map +1 -0
- package/dist/chat.d.ts +30 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +124 -0
- package/dist/chat.js.map +1 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -1
- package/dist/game.d.ts +7 -0
- package/dist/game.d.ts.map +1 -1
- package/dist/game.js +1 -0
- package/dist/game.js.map +1 -1
- package/dist/protocol.d.ts +35 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +6 -0
- package/dist/protocol.js.map +1 -1
- package/dist/room.d.ts +23 -0
- package/dist/room.d.ts.map +1 -1
- package/dist/room.js +66 -3
- package/dist/room.js.map +1 -1
- package/dist/table-fun.d.ts +63 -3
- package/dist/table-fun.d.ts.map +1 -1
- package/dist/table-fun.js +209 -11
- package/dist/table-fun.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +5 -2
- package/dist/worker.js.map +1 -1
- package/package.json +5 -1
- package/src/chat-words.ts +14 -0
- package/src/chat.ts +131 -0
- package/src/client.ts +64 -3
- package/src/game.ts +11 -0
- package/src/protocol.ts +33 -1
- package/src/room.ts +76 -4
- package/src/table-fun.ts +220 -12
- package/src/worker.ts +5 -2
package/src/table-fun.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - 公平:人人一样、免费;只是画面和声音,不停计时、不改规则;电脑的出牌不读这些。电脑的反应只在画面层(单机)或房间的互动钩子里算。
|
|
11
11
|
* - kit 不认识任何游戏:短语表、名场面的映射、电脑人物都由游戏给。
|
|
12
12
|
*/
|
|
13
|
+
import { CHAT_MAX, chatRejectText, type ChatBlockWhy } from './chat.js';
|
|
13
14
|
import { applyTheme, type KitTheme } from './theme.js';
|
|
14
15
|
import { PROPS, STICKERS, createFunLayer, propSvg, showSticker, stickerSvg, throwAt, type FunLayer, type FunPhase, type PropId, type RectLike, type StickerId } from './fun.js';
|
|
15
16
|
import {
|
|
@@ -79,6 +80,18 @@ export interface TableFunOptions {
|
|
|
79
80
|
send(id: number, to?: number): void;
|
|
80
81
|
/** 现在能不能发(好友房:坐下了才能),默认能 */
|
|
81
82
|
canSend?(): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* 首次提示"点头像扔道具"冒在哪个座位(0.17.0):返回座位号,或 null = 现在不合适(比如那家的出牌区上有牌,气泡会挡住),
|
|
85
|
+
* 组件过 2 秒再问。不给就挑第一个电脑(没有就第一个别人),和以前一样
|
|
86
|
+
*/
|
|
87
|
+
hintSeat?(): number | null;
|
|
88
|
+
/**
|
|
89
|
+
* 好友房聊天(0.18.0,docs/chat.md):给了它"互动"面板才有"聊天"页。好友房传 `(text) => client.chat(text)`,返回 RoomClient.chat 的结果;
|
|
90
|
+
* 收到的经 TableFun.chat 交进来。单机不给(单机没有聊天)
|
|
91
|
+
*/
|
|
92
|
+
chat?(text: string): ChatResultLike;
|
|
93
|
+
/** 设置里"聊天:开 / 不看"(和"互动"分开):不看时别人说的不冒气泡、不进记录,自己也没有聊天页;默认开 */
|
|
94
|
+
chatEnabled?(): boolean;
|
|
82
95
|
/** 设置里"互动:开 / 不看",关了别人的一律不播,单机电脑也不反应;默认开 */
|
|
83
96
|
enabled?(): boolean;
|
|
84
97
|
/** 屏蔽名单按这个分开(房间码;单机一个固定值),默认 'solo' */
|
|
@@ -114,6 +127,26 @@ const DEFAULT_LINES: Record<FunMood, readonly string[]> = {
|
|
|
114
127
|
};
|
|
115
128
|
|
|
116
129
|
const HINT_TEXT = '点头像扔道具,长按连扔';
|
|
130
|
+
|
|
131
|
+
/** RoomClient.chat 的结果里组件要看的部分(不 import client.ts,单机的游戏也能用组件) */
|
|
132
|
+
export type ChatResultLike = { ok: true } | { ok: false; code: string; why?: ChatBlockWhy; waitMs?: number };
|
|
133
|
+
|
|
134
|
+
/** 聊天:气泡停多久、页面里记最近几条(只在内存里) */
|
|
135
|
+
export const CHAT_UI = { bubbleMs: 4000, keep: 30 } as const;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 首次提示冒在哪个座位(纯函数,测试直接测):游戏给了 pick 就听它的,它给的座位空着或不存在当作"现在不行";
|
|
139
|
+
* 没给就挑第一个电脑,没有电脑挑第一个别人。null = 这会儿不提示,过一会儿再问
|
|
140
|
+
*/
|
|
141
|
+
export function hintSeatFor(seats: readonly (FunSeat | null)[], pick?: () => number | null): number | null {
|
|
142
|
+
if (pick) {
|
|
143
|
+
const at = pick();
|
|
144
|
+
return at !== null && Number.isInteger(at) && seats[at] ? at : null;
|
|
145
|
+
}
|
|
146
|
+
const i = seats.findIndex((s) => !!s?.bot);
|
|
147
|
+
const at = i >= 0 ? i : seats.findIndex((s) => !!s && !s.me);
|
|
148
|
+
return at >= 0 ? at : null;
|
|
149
|
+
}
|
|
117
150
|
const CSS_ID = 'arcade-table-fun-css';
|
|
118
151
|
const CSS = `
|
|
119
152
|
.atf-ui{position:fixed;inset:0;pointer-events:none;font-family:var(--ak-font,inherit);-webkit-tap-highlight-color:transparent}
|
|
@@ -133,6 +166,14 @@ const CSS = `
|
|
|
133
166
|
.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}
|
|
134
167
|
.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}
|
|
135
168
|
.atf-btn svg{width:22px;height:22px}
|
|
169
|
+
.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)}
|
|
170
|
+
.atf-say{white-space:normal;max-width:240px;word-break:break-all;font-weight:600}
|
|
171
|
+
.atf-log{display:flex;flex-direction:column;gap:4px;font-size:14px;line-height:1.4;min-height:44px}
|
|
172
|
+
.atf-log b{color:var(--ak-title,#f3d98b);font-weight:700;margin-right:4px}
|
|
173
|
+
.atf-log .atf-empty{opacity:.6}
|
|
174
|
+
.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}
|
|
175
|
+
.atf-note{font-size:13px;margin:4px 4px 0;opacity:.85;min-height:18px}
|
|
176
|
+
.atf-chip:disabled{opacity:.5;cursor:default}
|
|
136
177
|
.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}
|
|
137
178
|
.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}
|
|
138
179
|
.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)}
|
|
@@ -144,7 +185,7 @@ const CSS = `
|
|
|
144
185
|
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>';
|
|
145
186
|
|
|
146
187
|
type Rect = { left: number; top: number; width: number; height: number };
|
|
147
|
-
type Tab = 'phrase' | 'sticker' | 'prop';
|
|
188
|
+
type Tab = 'phrase' | 'sticker' | 'prop' | 'chat';
|
|
148
189
|
|
|
149
190
|
/** 挂子元素(Workers 的类型里也有一个 append,签名对不上,所以统一用 appendChild) */
|
|
150
191
|
function add(parent: Node, ...kids: Node[]): void {
|
|
@@ -177,6 +218,14 @@ export class TableFun {
|
|
|
177
218
|
private lastProp: PropId = 'tomato';
|
|
178
219
|
private lastSticker: StickerId = 'smug';
|
|
179
220
|
private timers = new Set<ReturnType<typeof setTimeout>>();
|
|
221
|
+
/** 聊天记录(最近 CHAT_UI.keep 条,只在内存里;关页面、换房间就没了) */
|
|
222
|
+
private chatLog: { seat: number; name: string; text: string }[] = [];
|
|
223
|
+
private chatLogEl: HTMLElement | null = null;
|
|
224
|
+
/** 每个人最近的那个聊天气泡:同一个人连说两条,后一条顶掉前一条 */
|
|
225
|
+
private says = new Map<number, HTMLElement>();
|
|
226
|
+
private unread = false;
|
|
227
|
+
/** 键盘弹出时跟着可见区挪面板(输入框聚焦时挂上) */
|
|
228
|
+
private vvFix: (() => void) | null = null;
|
|
180
229
|
private dead = false;
|
|
181
230
|
private readonly onResize = (): void => this.refresh();
|
|
182
231
|
|
|
@@ -258,6 +307,7 @@ export class TableFun {
|
|
|
258
307
|
b.className = 'atf-btn';
|
|
259
308
|
b.type = 'button';
|
|
260
309
|
b.innerHTML = `${SMILE}<span>互动</span>`;
|
|
310
|
+
b.classList.toggle('atf-dot', this.unread);
|
|
261
311
|
b.addEventListener('click', (e) => {
|
|
262
312
|
e.stopPropagation();
|
|
263
313
|
if (this.open?.dataset['kind'] === 'panel') this.closeAll();
|
|
@@ -457,10 +507,13 @@ export class TableFun {
|
|
|
457
507
|
if (!this.canSend() || !this.btn) return;
|
|
458
508
|
this.tab = tab;
|
|
459
509
|
const r = this.btn.getBoundingClientRect();
|
|
510
|
+
const hasChat = this.chatOn();
|
|
511
|
+
if (tab === 'chat' && !hasChat) tab = this.tab = 'phrase';
|
|
460
512
|
const b = this.box('panel', 440, 300, r);
|
|
461
513
|
const tabs = this.el('div', 'atf-row');
|
|
462
|
-
|
|
463
|
-
|
|
514
|
+
const list: [Tab, string][] = [['phrase', '短语'], ['sticker', '表情'], ['prop', '道具'], ...(hasChat ? [['chat', '聊天'] as [Tab, string]] : [])];
|
|
515
|
+
list.forEach(([t, label]) => {
|
|
516
|
+
const c = this.chip(label, `fun:tab:${t}`, () => this.openPanel(t), t === tab ? 'atf-on' : 'atf-soft');
|
|
464
517
|
c.style.flex = '1';
|
|
465
518
|
add(tabs, c);
|
|
466
519
|
});
|
|
@@ -475,6 +528,12 @@ export class TableFun {
|
|
|
475
528
|
const body = this.el('div', 'atf-scroll');
|
|
476
529
|
body.style.maxHeight = '224px';
|
|
477
530
|
body.style.marginTop = '8px';
|
|
531
|
+
if (tab === 'chat') {
|
|
532
|
+
add(b, body);
|
|
533
|
+
this.chatBody(b, body);
|
|
534
|
+
this.fit(b, r, 440);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
478
537
|
if (tab === 'phrase') {
|
|
479
538
|
const g = this.el('div', 'atf-grid');
|
|
480
539
|
g.style.gridTemplateColumns = 'repeat(3,1fr)';
|
|
@@ -511,6 +570,155 @@ export class TableFun {
|
|
|
511
570
|
closeAll(): void {
|
|
512
571
|
this.open?.remove();
|
|
513
572
|
this.open = null;
|
|
573
|
+
this.chatLogEl = null;
|
|
574
|
+
this.unhookViewport();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ───────────── 聊天(0.18.0) ─────────────
|
|
578
|
+
|
|
579
|
+
private chatOn(): boolean {
|
|
580
|
+
return !!this.o.chat && (this.o.chatEnabled?.() ?? true);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** 聊天页:上面最近 30 条(新的在下),下面一行输入框 + 发送,再下面一行小字(还能打几个字、为什么没发出去) */
|
|
584
|
+
private chatBody(b: HTMLDivElement, body: HTMLDivElement): void {
|
|
585
|
+
this.setUnread(false);
|
|
586
|
+
// 记录自己滚,输入框一直在下面看得见
|
|
587
|
+
const log = this.el('div', 'atf-log atf-scroll');
|
|
588
|
+
log.style.maxHeight = '160px';
|
|
589
|
+
this.chatLogEl = log;
|
|
590
|
+
this.renderLog();
|
|
591
|
+
add(body, log);
|
|
592
|
+
const row = this.el('div', 'atf-row');
|
|
593
|
+
row.style.marginTop = '8px';
|
|
594
|
+
row.style.flexWrap = 'nowrap';
|
|
595
|
+
const input = this.el('input', 'atf-input');
|
|
596
|
+
input.type = 'text';
|
|
597
|
+
input.enterKeyHint = 'send';
|
|
598
|
+
input.autocomplete = 'off';
|
|
599
|
+
input.placeholder = '说点什么';
|
|
600
|
+
input.dataset['key'] = 'fun:chatInput';
|
|
601
|
+
const send = this.chip('发送', 'fun:chatSend', () => go());
|
|
602
|
+
const note = this.el('div', 'atf-note');
|
|
603
|
+
const left = (): void => {
|
|
604
|
+
const n = CHAT_MAX - Array.from(input.value.trim()).length;
|
|
605
|
+
note.textContent = n < 0 ? `多了 ${-n} 个字` : `还能打 ${n} 个字`;
|
|
606
|
+
send.disabled = n < 0 || send.dataset['wait'] === '1';
|
|
607
|
+
};
|
|
608
|
+
const go = (): void => {
|
|
609
|
+
if (!input.value.trim() || send.disabled) return;
|
|
610
|
+
const r = this.o.chat!(input.value);
|
|
611
|
+
if (r.ok) {
|
|
612
|
+
input.value = '';
|
|
613
|
+
left();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
note.textContent = chatRejectText(r.code, r.why);
|
|
617
|
+
if (r.code === 'chat_too_fast' && r.waitMs) this.countdown(send, r.waitMs, left);
|
|
618
|
+
};
|
|
619
|
+
input.addEventListener('input', left);
|
|
620
|
+
input.addEventListener('keydown', (e) => {
|
|
621
|
+
e.stopPropagation();
|
|
622
|
+
if (e.key === 'Enter' && !e.isComposing) go();
|
|
623
|
+
});
|
|
624
|
+
input.addEventListener('focus', () => this.hookViewport(b));
|
|
625
|
+
add(row, input, send);
|
|
626
|
+
add(body, row, note);
|
|
627
|
+
left();
|
|
628
|
+
body.style.maxHeight = '';
|
|
629
|
+
log.scrollTop = log.scrollHeight;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** 被限速时"发送"按钮倒计时,到点恢复 */
|
|
633
|
+
private countdown(btn: HTMLButtonElement, ms: number, done: () => void): void {
|
|
634
|
+
btn.dataset['wait'] = '1';
|
|
635
|
+
btn.disabled = true;
|
|
636
|
+
const end = this.now() + ms;
|
|
637
|
+
const tick = (): void => {
|
|
638
|
+
const s = Math.ceil((end - this.now()) / 1000);
|
|
639
|
+
if (s <= 0 || !btn.isConnected) {
|
|
640
|
+
btn.textContent = '发送';
|
|
641
|
+
delete btn.dataset['wait'];
|
|
642
|
+
done();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
btn.textContent = `${s} 秒`;
|
|
646
|
+
this.later(Math.min(1000, end - this.now()), tick);
|
|
647
|
+
};
|
|
648
|
+
tick();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private renderLog(): void {
|
|
652
|
+
const log = this.chatLogEl;
|
|
653
|
+
if (!log) return;
|
|
654
|
+
log.replaceChildren();
|
|
655
|
+
if (this.chatLog.length === 0) add(log, this.el('div', 'atf-empty', '还没人说话。这里的话不保存,房间散了就没了'));
|
|
656
|
+
for (const m of this.chatLog) {
|
|
657
|
+
const row = this.el('div', '');
|
|
658
|
+
add(row, this.el('b', '', `${m.name}:`), this.doc.createTextNode(m.text));
|
|
659
|
+
add(log, row);
|
|
660
|
+
}
|
|
661
|
+
log.scrollTop = log.scrollHeight;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* iOS 键盘弹出时会盖住下半屏、页面不一定跟着滚:按 visualViewport 把面板整个往上挪到键盘上面,记录区限在可见高度的 60% 以内。
|
|
666
|
+
* 只动这个面板的 top(transform 里是 placeMenu 摆的位置),收起菜单时摘掉
|
|
667
|
+
*/
|
|
668
|
+
private hookViewport(b: HTMLDivElement): void {
|
|
669
|
+
const vv = window.visualViewport;
|
|
670
|
+
if (!vv || this.vvFix) return;
|
|
671
|
+
const fix = (): void => {
|
|
672
|
+
const log = this.chatLogEl;
|
|
673
|
+
if (log) log.style.maxHeight = `${Math.max(44, Math.floor(vv.height * 0.6) - 110)}px`;
|
|
674
|
+
b.style.top = '0px';
|
|
675
|
+
const r = b.getBoundingClientRect();
|
|
676
|
+
const bottom = vv.offsetTop + vv.height - 8;
|
|
677
|
+
if (r.bottom > bottom) b.style.top = `${Math.max(vv.offsetTop + 4 - r.top, bottom - r.bottom)}px`;
|
|
678
|
+
};
|
|
679
|
+
this.vvFix = fix;
|
|
680
|
+
vv.addEventListener('resize', fix);
|
|
681
|
+
vv.addEventListener('scroll', fix);
|
|
682
|
+
fix();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private unhookViewport(): void {
|
|
686
|
+
const vv = typeof window !== 'undefined' ? window.visualViewport : null;
|
|
687
|
+
if (this.vvFix && vv) {
|
|
688
|
+
vv.removeEventListener('resize', this.vvFix);
|
|
689
|
+
vv.removeEventListener('scroll', this.vvFix);
|
|
690
|
+
}
|
|
691
|
+
this.vvFix = null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private setUnread(on: boolean): void {
|
|
695
|
+
this.unread = on;
|
|
696
|
+
this.btn?.classList.toggle('atf-dot', on);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* 有人说了一句(好友房 RoomClient 的 onChat 交进来):头像旁冒 4 秒气泡、记进聊天页。
|
|
701
|
+
* 别人说的在"聊天:不看"、被屏蔽(单个或全部屏蔽)时一律不显示;面板没开在聊天页时"互动"按钮上亮个小红点
|
|
702
|
+
*/
|
|
703
|
+
chat(from: number, text: string, _masked = false): void {
|
|
704
|
+
if (this.dead || !this.o.chat) return;
|
|
705
|
+
const src = this.seat(from);
|
|
706
|
+
if (!src) return;
|
|
707
|
+
if (!src.me && (!(this.o.chatEnabled?.() ?? true) || this.isBlocked(from))) return;
|
|
708
|
+
this.chatLog.push({ seat: from, name: src.me ? '我' : src.name, text });
|
|
709
|
+
while (this.chatLog.length > CHAT_UI.keep) this.chatLog.shift();
|
|
710
|
+
this.says.get(from)?.remove();
|
|
711
|
+
const bb = this.bubble(from, text, CHAT_UI.bubbleMs, 'atf-say');
|
|
712
|
+
if (bb) this.says.set(from, bb);
|
|
713
|
+
if (this.chatLogEl) this.renderLog();
|
|
714
|
+
else if (!src.me) this.setUnread(true);
|
|
715
|
+
this.o.log?.(`聊天 ${from}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** 房间拒了一条(RoomClient 的 onChatError):聊天页开着就在小字那行说一声 */
|
|
719
|
+
chatError(code: string): void {
|
|
720
|
+
const note = this.open?.querySelector<HTMLElement>('.atf-note');
|
|
721
|
+
if (note) note.textContent = chatRejectText(code);
|
|
514
722
|
}
|
|
515
723
|
|
|
516
724
|
/** 有菜单或面板开着(游戏可以据此暂停自己的快捷键) */
|
|
@@ -641,19 +849,20 @@ export class TableFun {
|
|
|
641
849
|
|
|
642
850
|
// ───────────── 小效果 ─────────────
|
|
643
851
|
|
|
644
|
-
/** 头像旁的一句话:朝屏幕中间那侧冒,2.4
|
|
645
|
-
bubble(seat: number, text: string, ms = 2400):
|
|
852
|
+
/** 头像旁的一句话:朝屏幕中间那侧冒,2.4 秒后淡出;cls 加样式(聊天的气泡能折行) */
|
|
853
|
+
bubble(seat: number, text: string, ms = 2400, cls = ''): HTMLElement | undefined {
|
|
646
854
|
const s = this.seat(seat);
|
|
647
|
-
if (!s) return;
|
|
648
|
-
const b = this.el('div',
|
|
855
|
+
if (!s) return undefined;
|
|
856
|
+
const b = this.el('div', `atf-bubble ${cls}`.trim(), text);
|
|
649
857
|
this.layer.el.appendChild(b);
|
|
650
|
-
const w = Math.min(220, b.offsetWidth || text.length * 15 + 24);
|
|
858
|
+
const w = Math.min(cls ? 240 : 220, b.offsetWidth || text.length * 15 + 24);
|
|
651
859
|
const cx = s.rect.left + s.rect.width / 2;
|
|
652
860
|
const right = cx < window.innerWidth / 2;
|
|
653
861
|
const x = Math.max(4, Math.min(window.innerWidth - w - 4, right ? s.rect.left + s.rect.width + 6 : s.rect.left - 6 - w));
|
|
654
862
|
const y = Math.max(4, s.rect.top - 6);
|
|
655
863
|
b.style.transform = `translate(${x}px,${y}px)`;
|
|
656
864
|
void anim(b, [{ opacity: 0 }, { opacity: 1 }], 160).then(() => anim(b, [{ opacity: 1 }, { opacity: 0 }], 260, ms)).then(() => b.remove());
|
|
865
|
+
return b;
|
|
657
866
|
}
|
|
658
867
|
|
|
659
868
|
/** 头像上方冒一个字(×N、回敬!、护盾):放大弹一下再飘走 */
|
|
@@ -689,10 +898,8 @@ export class TableFun {
|
|
|
689
898
|
} catch {
|
|
690
899
|
return;
|
|
691
900
|
}
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
const at = i >= 0 ? i : seats.findIndex((s) => !!s && !s.me);
|
|
695
|
-
if (at < 0 || !this.canSend() || !this.enabled() || this.open) return this.later(2000, () => this.hint());
|
|
901
|
+
const at = hintSeatFor(this.o.seats(), this.o.hintSeat);
|
|
902
|
+
if (at === null || !this.canSend() || !this.enabled() || this.open) return this.later(2000, () => this.hint());
|
|
696
903
|
try {
|
|
697
904
|
localStorage.setItem(key, '1');
|
|
698
905
|
} catch {
|
|
@@ -732,6 +939,7 @@ export class TableFun {
|
|
|
732
939
|
|
|
733
940
|
destroy(): void {
|
|
734
941
|
this.dead = true;
|
|
942
|
+
this.unhookViewport();
|
|
735
943
|
for (const t of this.timers) clearTimeout(t);
|
|
736
944
|
this.timers.clear();
|
|
737
945
|
window.removeEventListener('resize', this.onResize);
|
package/src/worker.ts
CHANGED
|
@@ -131,8 +131,11 @@ export async function acceptJoin(request: Request, env: KitEnv, code: string, op
|
|
|
131
131
|
limited = true;
|
|
132
132
|
}
|
|
133
133
|
// 新建请求,只带认好的身份和客户端版本;浏览器带来的 x-kit-user 之类的头一律不转
|
|
134
|
-
const
|
|
135
|
-
const
|
|
134
|
+
const q = new URL(request.url).searchParams;
|
|
135
|
+
const cv = q.get('cv') ?? '0';
|
|
136
|
+
// chat=1(0.18.0):新版 RoomClient 认聊天消息,房间只给这样的连接发
|
|
137
|
+
const chat = q.get('chat') === '1' ? '&chat=1' : '';
|
|
138
|
+
const res = await roomStub(env.ROOM, code).fetch(`https://${KIT_HOST}/ws?cv=${encodeURIComponent(cv)}${chat}`, {
|
|
136
139
|
headers: { upgrade: 'websocket', 'x-kit-user': identityHeader(me), ...(limited ? { [LIMITED_HEADER]: '1' } : {}) },
|
|
137
140
|
});
|
|
138
141
|
if (!res.headers.has(MISS_HEADER)) return res;
|