dsh-dock 0.9.6 → 0.9.7

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.
Files changed (35) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +325 -325
  3. package/client.js +42 -8
  4. package/cordis.patch.yml +2 -2
  5. package/features/animation/host.js +660 -660
  6. package/features/animation/view.jsx +2247 -2247
  7. package/features/balance/host.js +494 -494
  8. package/features/balance/view.js +351 -351
  9. package/features/games/games/breakout.jsx +227 -227
  10. package/features/games/games/gomoku.jsx +244 -244
  11. package/features/games/games/racer.jsx +169 -169
  12. package/features/games/games/shared.jsx +118 -118
  13. package/features/games/games/snake.jsx +144 -144
  14. package/features/games/games/sokoban.jsx +180 -180
  15. package/features/games/games/tank.jsx +128 -128
  16. package/features/games/games/tetris.jsx +308 -308
  17. package/features/games/games/xiangqi.jsx +277 -277
  18. package/features/games/tank-core.js +152 -152
  19. package/features/games/view.jsx +639 -639
  20. package/features/games/xiangqi-core.js +299 -299
  21. package/features/heartbeat/view.js +35 -35
  22. package/features/mobile-relay/gateway.js +472 -472
  23. package/features/mobile-relay/host.js +570 -569
  24. package/features/mobile-relay/view.jsx +300 -300
  25. package/features/modelconfig/host.js +403 -403
  26. package/features/modelconfig/view.js +490 -490
  27. package/features/theme/view.js +61 -61
  28. package/features/tokenlog/host.js +678 -678
  29. package/features/tokenlog/view.jsx +561 -561
  30. package/features/visionproxy/host.js +315 -315
  31. package/index.js +175 -118
  32. package/package.json +59 -59
  33. package/src/client.jsx +668 -668
  34. package/src/host-core.js +98 -95
  35. package/src/shared.js +167 -130
@@ -1,144 +1,144 @@
1
- /* 趣味游戏 · 贪吃蛇(经典机台)——纯 Client;吃到食物变长,撞墙或咬到自身结束。 */
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { focusStage, useGameControls, playSfx } from "./shared.jsx";
4
-
5
- const SIZE = 16;
6
- const DIRS = { ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1], w: [-1, 0], s: [1, 0], a: [0, -1], d: [0, 1], W: [-1, 0], S: [1, 0], A: [0, -1], D: [0, 1] };
7
- const OPPOSITE = { "-1,0": [1, 0], "1,0": [-1, 0], "0,1": [0, -1], "0,-1": [0, 1] };
8
- // 三档速度:随得分缓慢提速,下限保证可玩性。
9
- const SPEED_MODES = {
10
- slow: { label: "慢速", stepMs: 4, base: 320, min: 180 },
11
- normal: { label: "中速", stepMs: 3, base: 220, min: 120 },
12
- fast: { label: "快速", stepMs: 2, base: 140, min: 80 },
13
- };
14
- function loadSpeedMode() {
15
- try { return SPEED_MODES[localStorage.getItem("dsh-dock.snake.speed")] ? localStorage.getItem("dsh-dock.snake.speed") : "slow"; } catch { return "slow"; }
16
- }
17
-
18
- function randFood(snake) {
19
- while (true) {
20
- const r = Math.floor(Math.random() * SIZE), c = Math.floor(Math.random() * SIZE);
21
- if (!snake.some((s) => s.r === r && s.c === c)) return { r, c };
22
- }
23
- }
24
- function initial() {
25
- const snake = [{ r: 7, c: 5 }, { r: 7, c: 4 }, { r: 7, c: 3 }];
26
- return { snake, dir: [0, 1], food: randFood(snake), score: 0, over: false, started: false };
27
- }
28
- function step(state, dir) {
29
- const head = state.snake[0];
30
- const nr = head.r + dir[0], nc = head.c + dir[1];
31
- const body = state.snake.slice(0, -1);
32
- if (nr < 0 || nr >= SIZE || nc < 0 || nc >= SIZE || body.some((s) => s.r === nr && s.c === nc)) {
33
- return Object.assign({}, state, { over: true });
34
- }
35
- const ate = state.food.r === nr && state.food.c === nc;
36
- let snake = [{ r: nr, c: nc }, ...state.snake];
37
- if (ate) {
38
- playSfx("eat");
39
- const food = randFood(snake);
40
- return Object.assign({}, state, { snake, food, score: state.score + 1 });
41
- }
42
- snake.pop();
43
- return Object.assign({}, state, { snake });
44
- }
45
-
46
- export function SnakeGame(props) {
47
- const [state, setState] = useState(initial);
48
- const [paused, setPaused] = useState(false);
49
- // 速度档位:默认慢速,可随时切换(重开不重置)。
50
- const [speedMode, setSpeedMode] = useState(loadSpeedMode);
51
- const stageRef = useRef(null);
52
- const dirRef = useRef(state.dir);
53
- const overRef = useRef(state.over); overRef.current = state.over;
54
- const pausedRef = useRef(!!(props && props.paused) || paused); pausedRef.current = !!(props && props.paused) || paused;
55
-
56
- const start = useCallback(() => { const init = initial(); init.started = true; dirRef.current = init.dir; setState(init); setPaused(false); }, []);
57
- // 待机 → 开始:按回车/空格,或直接按方向键起步。
58
- const begin = useCallback(() => { setState((s) => (s.started || s.over ? s : Object.assign({}, s, { started: true }))); }, []);
59
- const changeDir = useCallback((nv) => {
60
- if (overRef.current || pausedRef.current) return;
61
- const cur = dirRef.current;
62
- const opp = OPPOSITE[cur[0] + "," + cur[1]];
63
- if (opp && opp[0] === nv[0] && opp[1] === nv[1]) return;
64
- dirRef.current = nv;
65
- begin();
66
- }, [begin]);
67
-
68
- useEffect(() => {
69
- if (state.over) playSfx("over");
70
- }, [state.over]);
71
-
72
- useEffect(() => {
73
- if (!state.started || state.over || pausedRef.current) return;
74
- const mode = SPEED_MODES[speedMode] || SPEED_MODES.slow;
75
- const speed = Math.max(mode.min, mode.base - state.score * mode.stepMs);
76
- const timer = setInterval(() => setState((s) => (s.over || !s.started || pausedRef.current ? s : step(s, dirRef.current))), speed);
77
- return () => clearInterval(timer);
78
- }, [state.started, state.over, state.score, props.paused, paused, speedMode]);
79
-
80
- const pickSpeed = useCallback((m) => {
81
- setSpeedMode(m);
82
- try { localStorage.setItem("dsh-dock.snake.speed", m); } catch { /* ignore */ }
83
- }, []);
84
-
85
- const onKeyDown = useCallback((event) => {
86
- const d = DIRS[event.key];
87
- if (d) { event.preventDefault(); changeDir(d); return; }
88
- if (event.key === "Enter" || event.key === " ") {
89
- event.preventDefault();
90
- if (overRef.current || !state.started || pausedRef.current) start();
91
- else setPaused((p) => !p);
92
- return;
93
- }
94
- if (event.key.toLowerCase() === "p") { event.preventDefault(); setPaused((p) => !p); }
95
- else if (event.key.toLowerCase() === "r") { event.preventDefault(); start(); }
96
- }, [changeDir, start, state.started]);
97
-
98
- useGameControls(stageRef, pausedRef.current, onKeyDown);
99
-
100
- const cells = [];
101
- for (let r = 0; r < SIZE; r += 1) {
102
- for (let c = 0; c < SIZE; c += 1) {
103
- const idx = state.snake.findIndex((s) => s.r === r && s.c === c);
104
- const isFood = state.food.r === r && state.food.c === c;
105
- let cls = "dgame-snake-cell";
106
- if (idx >= 0) cls += " bob" + (idx === 0 ? " head" : idx < 4 ? " body2" : "");
107
- if (isFood) cls += " food";
108
- cells.push(<i key={r + "-" + c} className={cls} aria-hidden="true" />);
109
- }
110
- }
111
-
112
- return <section className="dgame-game" aria-label="贪吃蛇">
113
- <div className="dgame-game-head"><div><h3>贪吃蛇</h3><p>回车开始/暂停,方向键控制移动;吃到食物变长并缓慢提速,撞墙或咬到自己就结束。</p></div><div className="dgame-score"><span>长度 <strong>{state.score + 3}</strong></span><span>得分 {state.score}</span></div></div>
114
- <div className="dgame-snake-speeds" role="radiogroup" aria-label="游戏速度">
115
- <span>速度</span>
116
- {Object.entries(SPEED_MODES).map(([m, cfg]) => (
117
- <button key={m} type="button" role="radio" aria-checked={speedMode === m} className={speedMode === m ? "on" : ""} onClick={() => pickSpeed(m)}>{cfg.label}</button>
118
- ))}
119
- </div>
120
- <div className="dgame-snake-stage" ref={stageRef} tabIndex={0} onKeyDown={onKeyDown} onClick={() => focusStage(stageRef)} aria-label="贪吃蛇游戏区域,使用方向键控制移动,回车开始">
121
- <div className="dgame-snake-grid">{cells}</div>
122
- {state.over ? <div className="dgame-over"><strong>撞上了 · 得 {state.score} 分</strong><button type="button" onClick={start}>重新开始</button></div> : null}
123
- {!state.started && !state.over ? <div className="dgame-over"><strong>按 Enter 或方向键开始</strong><button type="button" onClick={start}>开始游戏</button></div> : null}
124
- {state.started && !state.over && pausedRef.current ? <div className="dgame-over"><strong>已暂停</strong><button type="button" onClick={() => setPaused(false)}>继续(P)</button></div> : null}
125
- </div>
126
- <div className="dgame-controls"><button type="button" aria-label="向上" onClick={() => changeDir([-1, 0])}>↑</button><span>方向键 / WASD · 回车 开始/暂停 · R 重开</span><button type="button" aria-label="向下" onClick={() => changeDir([1, 0])}>↓</button></div>
127
- </section>;
128
- }
129
-
130
- export function SnakePreview() {
131
- return <span className="dgcov-prev dgcov-prev-snake" aria-hidden="true">
132
- <i style={{ left: "12%", top: "62%" }} /><i style={{ left: "24%", top: "62%" }} /><i style={{ left: "36%", top: "62%" }} /><i style={{ left: "48%", top: "62%" }} /><i style={{ left: "60%", top: "62%" }} />
133
- <b />
134
- </span>;
135
- }
136
-
137
- export const snakeGame = { Game: SnakeGame, Preview: SnakePreview, css: `
138
- .dgame-snake-speeds{display:flex;gap:6px;align-items:center;align-self:center;justify-content:center}
139
- .dgame-snake-speeds span{font-size:11px;color:var(--dsw-alias-label-tertiary)}
140
- .dgame-snake-speeds button{min-width:44px;height:26px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;transition:all .15s ease}
141
- .dgame-snake-speeds button.on{color:#bbf7d0;border-color:color-mix(in srgb,#4ade80 55%,transparent);background:color-mix(in srgb,#4ade80 16%,transparent)}
142
- .dgame-snake-speeds button:hover{border-color:#4ade80;color:var(--dsw-alias-label-primary)}
143
- .dgame-snake-stage{position:relative;align-self:center;width:min(100%,300px);aspect-ratio:1/1;border:1px solid color-mix(in srgb,rgb(74 222 128) 32%,var(--dsw-alias-border-l1));border-radius:10px;overflow:hidden;outline:none;background:radial-gradient(circle at 50% -10%,color-mix(in srgb,#166534 30%,transparent),transparent 55%),#0a1420}.dgame-snake-stage:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(74 222 128) 35%,transparent)}.dgame-snake-grid{display:grid;grid-template-columns:repeat(16,1fr);grid-template-rows:repeat(16,1fr);width:100%;height:100%}.dgame-snake-cell{position:relative}.dgame-snake-cell.food::after{content:'';position:absolute;inset:22%;border-radius:50%;background:radial-gradient(circle at 35% 30%,#fecaca,#ef4444 70%);box-shadow:0 0 9px color-mix(in srgb,#ef4444 70%,transparent)}.dgame-snake-cell.bob::after{content:'';position:absolute;inset:20%;border-radius:26%;background:linear-gradient(135deg,#4ade80,#16a34a)}.dgame-snake-cell.head::after{background:linear-gradient(135deg,#bbf7d0,#22c55e);border-radius:30% 30% 28% 28%;box-shadow:0 1px 6px color-mix(in srgb,#22c55e 60%,transparent)}.dgame-snake-cell.head::before{content:'';position:absolute;left:30%;top:30%;width:9%;height:9%;border-radius:50%;background:#0b1b12}.dgame-snake-cell.body2::after{background:linear-gradient(135deg,#34d399,#0d9f6e)}
144
- ` };
1
+ /* 趣味游戏 · 贪吃蛇(经典机台)——纯 Client;吃到食物变长,撞墙或咬到自身结束。 */
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { focusStage, useGameControls, playSfx } from "./shared.jsx";
4
+
5
+ const SIZE = 16;
6
+ const DIRS = { ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1], w: [-1, 0], s: [1, 0], a: [0, -1], d: [0, 1], W: [-1, 0], S: [1, 0], A: [0, -1], D: [0, 1] };
7
+ const OPPOSITE = { "-1,0": [1, 0], "1,0": [-1, 0], "0,1": [0, -1], "0,-1": [0, 1] };
8
+ // 三档速度:随得分缓慢提速,下限保证可玩性。
9
+ const SPEED_MODES = {
10
+ slow: { label: "慢速", stepMs: 4, base: 320, min: 180 },
11
+ normal: { label: "中速", stepMs: 3, base: 220, min: 120 },
12
+ fast: { label: "快速", stepMs: 2, base: 140, min: 80 },
13
+ };
14
+ function loadSpeedMode() {
15
+ try { return SPEED_MODES[localStorage.getItem("dsh-dock.snake.speed")] ? localStorage.getItem("dsh-dock.snake.speed") : "slow"; } catch { return "slow"; }
16
+ }
17
+
18
+ function randFood(snake) {
19
+ while (true) {
20
+ const r = Math.floor(Math.random() * SIZE), c = Math.floor(Math.random() * SIZE);
21
+ if (!snake.some((s) => s.r === r && s.c === c)) return { r, c };
22
+ }
23
+ }
24
+ function initial() {
25
+ const snake = [{ r: 7, c: 5 }, { r: 7, c: 4 }, { r: 7, c: 3 }];
26
+ return { snake, dir: [0, 1], food: randFood(snake), score: 0, over: false, started: false };
27
+ }
28
+ function step(state, dir) {
29
+ const head = state.snake[0];
30
+ const nr = head.r + dir[0], nc = head.c + dir[1];
31
+ const body = state.snake.slice(0, -1);
32
+ if (nr < 0 || nr >= SIZE || nc < 0 || nc >= SIZE || body.some((s) => s.r === nr && s.c === nc)) {
33
+ return Object.assign({}, state, { over: true });
34
+ }
35
+ const ate = state.food.r === nr && state.food.c === nc;
36
+ let snake = [{ r: nr, c: nc }, ...state.snake];
37
+ if (ate) {
38
+ playSfx("eat");
39
+ const food = randFood(snake);
40
+ return Object.assign({}, state, { snake, food, score: state.score + 1 });
41
+ }
42
+ snake.pop();
43
+ return Object.assign({}, state, { snake });
44
+ }
45
+
46
+ export function SnakeGame(props) {
47
+ const [state, setState] = useState(initial);
48
+ const [paused, setPaused] = useState(false);
49
+ // 速度档位:默认慢速,可随时切换(重开不重置)。
50
+ const [speedMode, setSpeedMode] = useState(loadSpeedMode);
51
+ const stageRef = useRef(null);
52
+ const dirRef = useRef(state.dir);
53
+ const overRef = useRef(state.over); overRef.current = state.over;
54
+ const pausedRef = useRef(!!(props && props.paused) || paused); pausedRef.current = !!(props && props.paused) || paused;
55
+
56
+ const start = useCallback(() => { const init = initial(); init.started = true; dirRef.current = init.dir; setState(init); setPaused(false); }, []);
57
+ // 待机 → 开始:按回车/空格,或直接按方向键起步。
58
+ const begin = useCallback(() => { setState((s) => (s.started || s.over ? s : Object.assign({}, s, { started: true }))); }, []);
59
+ const changeDir = useCallback((nv) => {
60
+ if (overRef.current || pausedRef.current) return;
61
+ const cur = dirRef.current;
62
+ const opp = OPPOSITE[cur[0] + "," + cur[1]];
63
+ if (opp && opp[0] === nv[0] && opp[1] === nv[1]) return;
64
+ dirRef.current = nv;
65
+ begin();
66
+ }, [begin]);
67
+
68
+ useEffect(() => {
69
+ if (state.over) playSfx("over");
70
+ }, [state.over]);
71
+
72
+ useEffect(() => {
73
+ if (!state.started || state.over || pausedRef.current) return;
74
+ const mode = SPEED_MODES[speedMode] || SPEED_MODES.slow;
75
+ const speed = Math.max(mode.min, mode.base - state.score * mode.stepMs);
76
+ const timer = setInterval(() => setState((s) => (s.over || !s.started || pausedRef.current ? s : step(s, dirRef.current))), speed);
77
+ return () => clearInterval(timer);
78
+ }, [state.started, state.over, state.score, props.paused, paused, speedMode]);
79
+
80
+ const pickSpeed = useCallback((m) => {
81
+ setSpeedMode(m);
82
+ try { localStorage.setItem("dsh-dock.snake.speed", m); } catch { /* ignore */ }
83
+ }, []);
84
+
85
+ const onKeyDown = useCallback((event) => {
86
+ const d = DIRS[event.key];
87
+ if (d) { event.preventDefault(); changeDir(d); return; }
88
+ if (event.key === "Enter" || event.key === " ") {
89
+ event.preventDefault();
90
+ if (overRef.current || !state.started || pausedRef.current) start();
91
+ else setPaused((p) => !p);
92
+ return;
93
+ }
94
+ if (event.key.toLowerCase() === "p") { event.preventDefault(); setPaused((p) => !p); }
95
+ else if (event.key.toLowerCase() === "r") { event.preventDefault(); start(); }
96
+ }, [changeDir, start, state.started]);
97
+
98
+ useGameControls(stageRef, pausedRef.current, onKeyDown);
99
+
100
+ const cells = [];
101
+ for (let r = 0; r < SIZE; r += 1) {
102
+ for (let c = 0; c < SIZE; c += 1) {
103
+ const idx = state.snake.findIndex((s) => s.r === r && s.c === c);
104
+ const isFood = state.food.r === r && state.food.c === c;
105
+ let cls = "dgame-snake-cell";
106
+ if (idx >= 0) cls += " bob" + (idx === 0 ? " head" : idx < 4 ? " body2" : "");
107
+ if (isFood) cls += " food";
108
+ cells.push(<i key={r + "-" + c} className={cls} aria-hidden="true" />);
109
+ }
110
+ }
111
+
112
+ return <section className="dgame-game" aria-label="贪吃蛇">
113
+ <div className="dgame-game-head"><div><h3>贪吃蛇</h3><p>回车开始/暂停,方向键控制移动;吃到食物变长并缓慢提速,撞墙或咬到自己就结束。</p></div><div className="dgame-score"><span>长度 <strong>{state.score + 3}</strong></span><span>得分 {state.score}</span></div></div>
114
+ <div className="dgame-snake-speeds" role="radiogroup" aria-label="游戏速度">
115
+ <span>速度</span>
116
+ {Object.entries(SPEED_MODES).map(([m, cfg]) => (
117
+ <button key={m} type="button" role="radio" aria-checked={speedMode === m} className={speedMode === m ? "on" : ""} onClick={() => pickSpeed(m)}>{cfg.label}</button>
118
+ ))}
119
+ </div>
120
+ <div className="dgame-snake-stage" ref={stageRef} tabIndex={0} onKeyDown={onKeyDown} onClick={() => focusStage(stageRef)} aria-label="贪吃蛇游戏区域,使用方向键控制移动,回车开始">
121
+ <div className="dgame-snake-grid">{cells}</div>
122
+ {state.over ? <div className="dgame-over"><strong>撞上了 · 得 {state.score} 分</strong><button type="button" onClick={start}>重新开始</button></div> : null}
123
+ {!state.started && !state.over ? <div className="dgame-over"><strong>按 Enter 或方向键开始</strong><button type="button" onClick={start}>开始游戏</button></div> : null}
124
+ {state.started && !state.over && pausedRef.current ? <div className="dgame-over"><strong>已暂停</strong><button type="button" onClick={() => setPaused(false)}>继续(P)</button></div> : null}
125
+ </div>
126
+ <div className="dgame-controls"><button type="button" aria-label="向上" onClick={() => changeDir([-1, 0])}>↑</button><span>方向键 / WASD · 回车 开始/暂停 · R 重开</span><button type="button" aria-label="向下" onClick={() => changeDir([1, 0])}>↓</button></div>
127
+ </section>;
128
+ }
129
+
130
+ export function SnakePreview() {
131
+ return <span className="dgcov-prev dgcov-prev-snake" aria-hidden="true">
132
+ <i style={{ left: "12%", top: "62%" }} /><i style={{ left: "24%", top: "62%" }} /><i style={{ left: "36%", top: "62%" }} /><i style={{ left: "48%", top: "62%" }} /><i style={{ left: "60%", top: "62%" }} />
133
+ <b />
134
+ </span>;
135
+ }
136
+
137
+ export const snakeGame = { Game: SnakeGame, Preview: SnakePreview, css: `
138
+ .dgame-snake-speeds{display:flex;gap:6px;align-items:center;align-self:center;justify-content:center}
139
+ .dgame-snake-speeds span{font-size:11px;color:var(--dsw-alias-label-tertiary)}
140
+ .dgame-snake-speeds button{min-width:44px;height:26px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;transition:all .15s ease}
141
+ .dgame-snake-speeds button.on{color:#bbf7d0;border-color:color-mix(in srgb,#4ade80 55%,transparent);background:color-mix(in srgb,#4ade80 16%,transparent)}
142
+ .dgame-snake-speeds button:hover{border-color:#4ade80;color:var(--dsw-alias-label-primary)}
143
+ .dgame-snake-stage{position:relative;align-self:center;width:min(100%,300px);aspect-ratio:1/1;border:1px solid color-mix(in srgb,rgb(74 222 128) 32%,var(--dsw-alias-border-l1));border-radius:10px;overflow:hidden;outline:none;background:radial-gradient(circle at 50% -10%,color-mix(in srgb,#166534 30%,transparent),transparent 55%),#0a1420}.dgame-snake-stage:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,rgb(74 222 128) 35%,transparent)}.dgame-snake-grid{display:grid;grid-template-columns:repeat(16,1fr);grid-template-rows:repeat(16,1fr);width:100%;height:100%}.dgame-snake-cell{position:relative}.dgame-snake-cell.food::after{content:'';position:absolute;inset:22%;border-radius:50%;background:radial-gradient(circle at 35% 30%,#fecaca,#ef4444 70%);box-shadow:0 0 9px color-mix(in srgb,#ef4444 70%,transparent)}.dgame-snake-cell.bob::after{content:'';position:absolute;inset:20%;border-radius:26%;background:linear-gradient(135deg,#4ade80,#16a34a)}.dgame-snake-cell.head::after{background:linear-gradient(135deg,#bbf7d0,#22c55e);border-radius:30% 30% 28% 28%;box-shadow:0 1px 6px color-mix(in srgb,#22c55e 60%,transparent)}.dgame-snake-cell.head::before{content:'';position:absolute;left:30%;top:30%;width:9%;height:9%;border-radius:50%;background:#0b1b12}.dgame-snake-cell.body2::after{background:linear-gradient(135deg,#34d399,#0d9f6e)}
144
+ ` };