dsh-piggy 0.25.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +1348 -0
  2. package/LICENSE +32 -0
  3. package/README.md +322 -0
  4. package/THIRD-PARTY.md +31 -0
  5. package/assets/elder.svg +35 -0
  6. package/assets/pig-king-bathe.svg +38 -0
  7. package/assets/pig-king-eat.svg +45 -0
  8. package/assets/pig-king-pet.svg +45 -0
  9. package/assets/pig-king-play.svg +45 -0
  10. package/assets/pig-king-relaxed.svg +56 -0
  11. package/assets/pig-king-study.svg +45 -0
  12. package/assets/pig-king-trip.svg +45 -0
  13. package/assets/pig-king-work.svg +45 -0
  14. package/assets/pig-king.svg +56 -0
  15. package/assets/piglet.svg +30 -0
  16. package/client.js +3438 -0
  17. package/commands.js +252 -0
  18. package/cordis.patch.yml +15 -0
  19. package/core.js +11 -0
  20. package/data.js +10 -0
  21. package/index.js +68 -0
  22. package/package.json +72 -0
  23. package/packages/pet-core/package.json +21 -0
  24. package/packages/pet-core/src/core/activity.js +80 -0
  25. package/packages/pet-core/src/core/care.js +160 -0
  26. package/packages/pet-core/src/core/clock.js +122 -0
  27. package/packages/pet-core/src/core/constants.js +109 -0
  28. package/packages/pet-core/src/core/daily.js +204 -0
  29. package/packages/pet-core/src/core/diary.js +108 -0
  30. package/packages/pet-core/src/core/drops.js +82 -0
  31. package/packages/pet-core/src/core/effects.js +84 -0
  32. package/packages/pet-core/src/core/egg.js +120 -0
  33. package/packages/pet-core/src/core/evolution.js +90 -0
  34. package/packages/pet-core/src/core/growth.js +110 -0
  35. package/packages/pet-core/src/core/illness.js +235 -0
  36. package/packages/pet-core/src/core/interests.js +55 -0
  37. package/packages/pet-core/src/core/inventory.js +177 -0
  38. package/packages/pet-core/src/core/lines.js +157 -0
  39. package/packages/pet-core/src/core/migrate.js +226 -0
  40. package/packages/pet-core/src/core/profile.js +102 -0
  41. package/packages/pet-core/src/core/random.js +79 -0
  42. package/packages/pet-core/src/core/school.js +81 -0
  43. package/packages/pet-core/src/core/settlement.js +308 -0
  44. package/packages/pet-core/src/core/state.js +210 -0
  45. package/packages/pet-core/src/core/travel.js +58 -0
  46. package/packages/pet-core/src/core/upgrades.js +193 -0
  47. package/packages/pet-core/src/core/views.js +55 -0
  48. package/packages/pet-core/src/core/work.js +50 -0
  49. package/packages/pet-core/src/core.js +95 -0
  50. package/packages/pet-core/src/data/daily.js +121 -0
  51. package/packages/pet-core/src/data/drops.js +34 -0
  52. package/packages/pet-core/src/data/evolution.js +41 -0
  53. package/packages/pet-core/src/data/growth.js +72 -0
  54. package/packages/pet-core/src/data/illness.js +188 -0
  55. package/packages/pet-core/src/data/interests.js +39 -0
  56. package/packages/pet-core/src/data/jobs.js +150 -0
  57. package/packages/pet-core/src/data/life.js +88 -0
  58. package/packages/pet-core/src/data/lines.js +198 -0
  59. package/packages/pet-core/src/data/minutes.js +23 -0
  60. package/packages/pet-core/src/data/profile.js +60 -0
  61. package/packages/pet-core/src/data/school.js +99 -0
  62. package/packages/pet-core/src/data/shop.js +142 -0
  63. package/packages/pet-core/src/data/traits.js +42 -0
  64. package/packages/pet-core/src/data/travel.js +87 -0
  65. package/packages/pet-core/src/data.js +52 -0
  66. package/packages/pet-core/src/index.js +12 -0
  67. package/render.js +357 -0
  68. package/routes.js +201 -0
  69. package/scripts/build-client.mjs +100 -0
  70. package/scripts/typecheck.mjs +122 -0
  71. package/snapshot.js +368 -0
  72. package/src/client/art.js +29 -0
  73. package/src/client/constants.js +88 -0
  74. package/src/client/css-base.js +225 -0
  75. package/src/client/css-card.js +63 -0
  76. package/src/client/css-tabs.js +250 -0
  77. package/src/client/css-tiles.js +89 -0
  78. package/src/client/dom.js +33 -0
  79. package/src/client/effects.js +159 -0
  80. package/src/client/format.js +27 -0
  81. package/src/client/index.js +393 -0
  82. package/src/client/io.js +100 -0
  83. package/src/client/layout.js +103 -0
  84. package/src/client/normalize.js +352 -0
  85. package/src/client/panel.js +319 -0
  86. package/src/client/scene.js +113 -0
  87. package/src/client/storage.js +19 -0
  88. package/src/client/styles.js +13 -0
  89. package/src/client/tabs/bag.js +203 -0
  90. package/src/client/tabs/card.js +110 -0
  91. package/src/client/tabs/crown.js +59 -0
  92. package/src/client/tabs/dev.js +101 -0
  93. package/src/client/tabs/home.js +72 -0
  94. package/src/client/tabs/shop.js +78 -0
  95. package/src/client/tabs/status.js +223 -0
  96. package/src/client/tabs/study.js +128 -0
  97. package/src/client/tabs/travel.js +89 -0
  98. package/src/client/tabs/update.js +164 -0
  99. package/src/client/tabs/work.js +106 -0
  100. package/src/client/values.js +11 -0
  101. package/src/client/widgets.js +126 -0
  102. package/store/api.js +260 -0
  103. package/store/state-file.js +129 -0
  104. package/store.js +96 -0
@@ -0,0 +1,33 @@
1
+ // @ts-check
2
+ /**
3
+ * 极小的 DOM 构造助手。
4
+ *
5
+ * @module dsh-piggy/client/dom
6
+ */
7
+
8
+ import { num } from './values.js'
9
+ export function el(tag, className, text) {
10
+ var node = document.createElement(tag)
11
+ if (className) node.className = className
12
+ if (text !== undefined) node.textContent = text
13
+ return node
14
+ }
15
+
16
+ export function button(className, attrs, onClick) {
17
+ var node = el('button', className)
18
+ node.type = 'button'
19
+ for (var key in attrs) node.setAttribute(key, attrs[key])
20
+ node.addEventListener('click', function (event) {
21
+ event.stopPropagation()
22
+ onClick()
23
+ })
24
+ return node
25
+ }
26
+
27
+ export function meter(value, variant) {
28
+ var wrap = el('div', 'dp-meter' + (variant ? ' ' + variant : ''))
29
+ var fill = document.createElement('i')
30
+ fill.style.width = Math.max(0, Math.min(100, num(value, 0))) + '%'
31
+ wrap.appendChild(fill)
32
+ return wrap
33
+ }
@@ -0,0 +1,159 @@
1
+ // @ts-check
2
+ /**
3
+ * 猪的即时反馈:动作动画、粒子、气泡台词与 toast。
4
+ *
5
+ * 只碰交给它的几个元素,不读全局状态 —— 「猪还在不在」通过 isStopped()
6
+ * 回调问外壳(见 docs/CONVENTIONS.md)。
7
+ * @module dsh-piggy/client/effects
8
+ */
9
+ import { syncPigArt } from './art.js'
10
+ import { PET_LINES } from './constants.js'
11
+ import { el } from './dom.js'
12
+
13
+ /**
14
+ * @param {{ scene: object, pig: object, pigArt: object, card: object, bubble: object, isStopped: () => boolean }} deps
15
+ * @returns {{ react: Function, burst: Function, flash: Function, showBubble: Function, showLine: Function, toast: Function, dispose: Function }}
16
+ */
17
+ export function createEffects(deps) {
18
+ var scene = deps.scene
19
+ var pig = deps.pig
20
+ var card = deps.card
21
+ var bubble = deps.bubble
22
+ var isStopped = deps.isStopped
23
+
24
+ var reactTimer = null
25
+ var bubbleTimer = null
26
+
27
+ // ---- animation ----
28
+ function react(kind, ms) {
29
+ if (reactTimer !== null) window.clearTimeout(reactTimer)
30
+ // Re-assigning the same value does NOT restart a CSS animation, so
31
+ // clicking three times quickly only played it once. Dropping the
32
+ // attribute and forcing a reflow makes every click start from zero.
33
+ pig.removeAttribute('data-react')
34
+ void pig.offsetWidth
35
+ pig.setAttribute('data-react', kind)
36
+ syncPigArt(pig, deps.pigArt)
37
+ reactTimer = window.setTimeout(function () {
38
+ pig.removeAttribute('data-react')
39
+ syncPigArt(pig, deps.pigArt)
40
+ reactTimer = null
41
+ }, ms || 900)
42
+ }
43
+
44
+ function burst(emojis, count) {
45
+ for (var i = 0; i < (count || 1); i += 1) {
46
+ (function (index) {
47
+ window.setTimeout(function () {
48
+ if (isStopped()) return
49
+ var node = el('span', 'dp-fx', emojis[index % emojis.length])
50
+ node.style.setProperty('--dx', Math.round((Math.random() - 0.5) * 46) + 'px')
51
+ // Anchor to the pig, not the scene. The scene is panel-wide when
52
+ // open, so fixed coordinates put every particle off to one side.
53
+ var spot = headSpot()
54
+ node.style.left = (spot.x + Math.round((Math.random() - 0.5) * 22)) + 'px'
55
+ node.style.top = spot.y + 'px'
56
+ scene.appendChild(node)
57
+ window.setTimeout(function () { node.remove() }, 1200)
58
+ }, index * 110)
59
+ })(i)
60
+ }
61
+ }
62
+
63
+ /** Just above the pig's head, in scene coordinates. */
64
+ function headSpot() {
65
+ var fallback = { x: 24, y: 8 }
66
+ if (typeof pig.getBoundingClientRect !== 'function' || typeof scene.getBoundingClientRect !== 'function') return fallback
67
+ var p = pig.getBoundingClientRect()
68
+ var s = scene.getBoundingClientRect()
69
+ if (p.width === 0 && p.height === 0) return fallback
70
+ return { x: p.left - s.left + p.width / 2, y: p.top - s.top - 20 }
71
+ }
72
+
73
+ var REACTIONS = {
74
+ hatch: { kind: 'levelup', ms: 980, fx: ['🥚', '✨', '🐖', '🎉'], count: 4, say: '孵出来啦!' },
75
+ feed: { kind: 'feed', ms: 900, fx: ['🍎', '😋', '✨'], count: 3, say: '吃掉了!' },
76
+ bathe: { kind: 'bathe', ms: 1050, fx: ['🫧', '🫧', '💧', '✨'], count: 4, say: '洗干净啦~' },
77
+ play: { kind: 'play', ms: 900, fx: ['🎾', '⭐', '💨'], count: 3, say: '好开心!' },
78
+ pet: { kind: 'pet', ms: 420, fx: ['❤️'], count: 1, say: '好舒服…' },
79
+ work: { kind: 'away', ms: 900, fx: ['💼', '🧱', '🪙'], count: 3, say: '出门打工!' },
80
+ study: { kind: 'away', ms: 900, fx: ['📚', '✏️', '🧠'], count: 3, say: '上学去!' },
81
+ trip: { kind: 'away', ms: 900, fx: ['🧳', '🗺', '✨'], count: 3, say: '出发旅行!' },
82
+ calloff: { kind: 'refuse', ms: 520, fx: ['💨'], count: 1, say: '提前回来了…' },
83
+ buy: { kind: 'pet', ms: 620, fx: ['🪙', '🛒'], count: 2, say: '买到了!' },
84
+ use: { kind: 'pet', ms: 620, fx: ['✨'], count: 2, say: '用掉了。' },
85
+ }
86
+
87
+ function flash(action) {
88
+ var spec = REACTIONS[action]
89
+ if (spec === undefined) return
90
+ react(spec.kind, spec.ms)
91
+ burst(spec.fx, spec.count)
92
+ // Patting is the one thing you do over and over, so it gets a pool of
93
+ // lines rather than the same four characters every time.
94
+ var lines = action === 'pet' ? PET_LINES : null
95
+ showBubble(lines === null ? spec.say : lines[Math.floor(Math.random() * lines.length)], 1600)
96
+ }
97
+
98
+ var bubbleTimer = null
99
+ function showBubble(text, ms) {
100
+ if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
101
+ bubble.textContent = text
102
+ bubble.hidden = false
103
+ bubbleTimer = window.setTimeout(function () {
104
+ bubble.hidden = true
105
+ bubbleTimer = null
106
+ }, ms || 2600)
107
+ }
108
+
109
+ /**
110
+ * A line the pig says, with optional reply buttons. Replying closes the bubble;
111
+ * a line with buttons stays up longer so there is time to answer it.
112
+ * @param {string} text
113
+ * @param {string[]} replies
114
+ * @param {(index: number) => void} onReply
115
+ */
116
+ function showLine(text, replies, onReply) {
117
+ if (replies.length === 0) {
118
+ showBubble(text, 2600)
119
+ return
120
+ }
121
+ if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
122
+ bubble.textContent = text
123
+ var row = el('div', 'dp-bubble-replies', '')
124
+ replies.forEach(function (label, index) {
125
+ var answer = el('button', 'dp-reply', label)
126
+ answer.type = 'button'
127
+ answer.addEventListener('click', function (event) {
128
+ // The bubble sits over the pig; a reply must not also count as a pat.
129
+ event.stopPropagation()
130
+ bubble.hidden = true
131
+ if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
132
+ bubbleTimer = null
133
+ onReply(index)
134
+ })
135
+ row.appendChild(answer)
136
+ })
137
+ bubble.appendChild(row)
138
+ bubble.hidden = false
139
+ bubbleTimer = window.setTimeout(function () {
140
+ bubble.hidden = true
141
+ bubbleTimer = null
142
+ }, 6000)
143
+ }
144
+
145
+ function toast(text) {
146
+ var node = el('div', 'dp-toast', text)
147
+ card.insertBefore(node, card.firstChild)
148
+ window.setTimeout(function () { node.remove() }, 4800)
149
+ }
150
+ /** Drop the pending timers — the panel is going away, nothing should fire. */
151
+ function dispose() {
152
+ if (reactTimer !== null) window.clearTimeout(reactTimer)
153
+ if (bubbleTimer !== null) window.clearTimeout(bubbleTimer)
154
+ reactTimer = null
155
+ bubbleTimer = null
156
+ }
157
+
158
+ return { react: react, burst: burst, flash: flash, showBubble: showBubble, showLine: showLine, toast: toast, dispose: dispose }
159
+ }
@@ -0,0 +1,27 @@
1
+ // @ts-check
2
+ /**
3
+ * 客户端小格式化:天数、分钟数、物品类别名。
4
+ *
5
+ * 纯函数,输入输出都是字符串(见 docs/CONVENTIONS.md)。
6
+ * @module dsh-piggy/client/format
7
+ */
8
+
9
+ export function formatDays(days) {
10
+ if (days >= 1) return Math.round(days) + ' 天'
11
+ const hours = days * 24
12
+ return hours >= 1 ? Math.round(hours) + ' 小时' : Math.max(1, Math.round(hours * 60)) + ' 分钟'
13
+ }
14
+
15
+ export function formatMinutes(minutes) {
16
+ if (minutes < 60) return minutes + ' 分钟'
17
+ const hours = Math.floor(minutes / 60)
18
+ const rest = minutes % 60
19
+ return rest === 0 ? hours + ' 小时' : hours + ' 小时' + rest + ' 分'
20
+ }
21
+
22
+ export function kindLabel(item) {
23
+ if (item.kind === 'medicine') return item.needed ? '对症!' : '药'
24
+ if (item.kind === 'revive') return '复活用'
25
+ if (item.kind === 'bath') return '洗浴'
26
+ return '食物'
27
+ }
@@ -0,0 +1,393 @@
1
+ /**
2
+ * dsh-piggy · client — the floating pig.
3
+ *
4
+ * Hand-written browser bundle: DSH loads it through `window.__ModuleLoader__`,
5
+ * bundled by esbuild from the modules beside it: raw DOM and `fetch` only, no framework.
6
+ *
7
+ * Layout follows the original QQ 宠物: the pet sits at the top, and a cream icon
8
+ * bar sits directly beneath it. Six icons — 状态 · 学习 · 打工 · 商店 · 旅行 ·
9
+ * 背包 — switch what the area below the bar shows, so nothing ever needs typing.
10
+ *
11
+ * Every value the host sends goes through `normalize()` first. A host/client
12
+ * version mismatch must degrade to sane defaults and an explanation, never to a
13
+ * screen full of `undefined`.
14
+ */
15
+
16
+ import { renderBagTab } from './tabs/bag.js'
17
+ import { renderDevTab } from './tabs/dev.js'
18
+ import { renderShopTab } from './tabs/shop.js'
19
+ import { renderStatusTab } from './tabs/status.js'
20
+ import { renderStudyTab } from './tabs/study.js'
21
+ import { renderTravelTab } from './tabs/travel.js'
22
+ import { renderWorkTab } from './tabs/work.js'
23
+ import { createEffects } from './effects.js'
24
+ import { createIo } from './io.js'
25
+ import { createLayout } from './layout.js'
26
+ import { createPanel } from './panel.js'
27
+ import { createScene } from './scene.js'
28
+ import { CSS } from './styles.js'
29
+ import { ACT_URL, ART_URL, BOX_POKES_TO_OPEN, BOX_POKE_LINES, CARE_LABEL, DEV_KEY, DEV_TAB, KIND_ORDER, KIND_TITLE, MOUNTED, MODES, NO_ITEM_LINE, OPEN_KEY, PANEL_GAP, PANEL_MARGIN, PANEL_MIN_HEIGHT, PANEL_WIDTH, PET_LINES, PIG_PADDING_X, GREET_DELAY_MS, IDLE_CHAT_MINUTES, POLL_MS, POSITION_KEY, SCENE_RESERVE, STAGES, STATE_URL, TABS } from './constants.js'
30
+ import { button, el, meter } from './dom.js'
31
+ import { normalize } from './normalize.js'
32
+ import { readStore, writeStore } from './storage.js'
33
+ import { arr, num, obj, str } from './values.js'
34
+
35
+ /** @type {any} */ (window).__ModuleLoader__.load({
36
+ id: 'dsh-piggy',
37
+ factory: (require) => {
38
+ var module = { exports: {} }
39
+ var exports = module.exports
40
+
41
+ var devMode = false
42
+
43
+ function apply(ctx) {
44
+ // A client plugin that throws while activating can take the whole web boot
45
+ // down with it, so the pig never lets an exception escape.
46
+ try {
47
+ return mount()
48
+ } catch (error) {
49
+ console.warn('[dsh-piggy] 挂载失败,猪先退到一边', error)
50
+ return () => {}
51
+ }
52
+ }
53
+
54
+ function mount() {
55
+ if (document.querySelector('[' + MOUNTED + ']') !== null) {
56
+ console.warn('[dsh-piggy] 已存在实例,跳过重复挂载')
57
+ return () => {}
58
+ }
59
+
60
+ // Nunito + Noto Sans SC, per the design system's standalone recipe. The
61
+ // request is non-blocking (`display=swap`) and the token font stack falls
62
+ // back to system faces, so an offline or blocked load degrades quietly
63
+ // instead of breaking the panel.
64
+ var parts = createScene()
65
+ var { font, style, host, card, scene, hud, hudName, hudCoins, hudHealth, bubble, work, prop,
66
+ progressWrap, progressFill, pokeHint, dailyHint, soul, pigArt, pigEmoji, pig, dressSlots, bar, content } = parts
67
+
68
+ var savedPos = readStore(POSITION_KEY)
69
+ // The pig's position as the user set it, before any on-screen clamp.
70
+ var userRight = 18
71
+ var userBottom = 18
72
+ if (savedPos !== null) {
73
+ try {
74
+ var parsed = JSON.parse(savedPos)
75
+ if (parsed && typeof parsed.right === 'number') userRight = parsed.right
76
+ if (parsed && typeof parsed.bottom === 'number') userBottom = parsed.bottom
77
+ } catch (error) { /* ignore */ }
78
+ }
79
+ host.style.right = userRight + 'px'
80
+ host.style.bottom = userBottom + 'px'
81
+
82
+ // Keep the pig itself on screen — and nothing more. There used to be a
83
+ // composer-avoidance floor here that forced the widget above the input box:
84
+ // it existed because the wrapper swallowed clicks aimed at the send button.
85
+ // `pointer-events:none` solves that properly now, so the clamp only ever
86
+ // stopped the user from parking their pet where they wanted it.
87
+
88
+ /**
89
+ * Place the panel so it is fully on screen, wherever the pig has been
90
+ * parked. The pig itself is never moved by this: the panel is absolutely
91
+ * positioned, so it takes no space in the wrapper's box.
92
+ */
93
+
94
+ var icons = {}
95
+
96
+ /** The tabs on show right now: the normal six, plus 调试 when dev mode is on. */
97
+
98
+ /** Rebuild the icon bar. Called whenever dev mode flips. */
99
+
100
+ // ---- state ----
101
+ var view = normalize(null)
102
+ // B9: the panel opens on the home screen of app tiles.
103
+ var tab = 'home'
104
+ var stage = 'primary'
105
+ // 用户自己点过学段之后,轮询就不许再替他改(B1 的「默认学段」只在没选过时生效)。
106
+ var stagePicked = false
107
+ // B8: which category each tile tab is opened into (null = the top layer), and a picked tile inside it.
108
+ var drill = { study: null, shop: null, bag: null, work: null, pick: null }
109
+ // Which souvenir's story card is open in the travel tab, if any.
110
+ var souvenirPick = null
111
+ // Which care action's item picker is open, if any.
112
+ var picker = null
113
+ // The owner-name draft while it is being edited on the status tab (null = not editing).
114
+ var ownerEdit = null
115
+ // 猪的名字草稿(同上:编辑期间轮询不许重绘,否则输入框会丢焦点)。
116
+ var pigNameEdit = null
117
+ // 居民卡: { field: 'catchphrase'|'motto', draft } while one is being edited.
118
+ var cardEdit = null
119
+
120
+ /** The tabs get an explicit context instead of closing over the shell locals. */
121
+ var isOpen = readStore(OPEN_KEY) === 'true'
122
+ var lastStage = null
123
+ var lastPendingAt = 0
124
+ var lastPendingId = 0
125
+ var pollTimer = null
126
+ var fx = createEffects({
127
+ scene: scene, pig: pig, pigArt: pigArt, card: card, bubble: bubble,
128
+ isStopped: function () { return stopped },
129
+ })
130
+ var react = fx.react, burst = fx.burst, flash = fx.flash
131
+ var showBubble = fx.showBubble, showLine = fx.showLine, toast = fx.toast
132
+ var stopped = false
133
+ var busy = false
134
+
135
+ /** The shell hands the modules an explicit context instead of sharing a scope. */
136
+ var ctx = {
137
+ host: host,
138
+ card: card,
139
+ content: content,
140
+ scene: scene,
141
+ hud: hud,
142
+ hudName: hudName,
143
+ hudCoins: hudCoins,
144
+ hudHealth: hudHealth,
145
+ bubble: bubble,
146
+ work: work,
147
+ prop: prop,
148
+ progressWrap: progressWrap,
149
+ progressFill: progressFill,
150
+ pokeHint: pokeHint,
151
+ dailyHint: dailyHint,
152
+ soul: soul,
153
+ pigArt: pigArt,
154
+ pigEmoji: pigEmoji,
155
+ pig: pig,
156
+ dressSlots: dressSlots,
157
+ bar: bar,
158
+ icons: icons,
159
+ flash: flash,
160
+ react: react,
161
+ burst: burst,
162
+ showBubble: showBubble,
163
+ showLine: showLine,
164
+ toast: toast,
165
+ get view() { return view }, set view(next) { view = next },
166
+ get tab() { return tab }, set tab(next) { tab = next },
167
+ get stage() { return stage }, set stage(next) { stage = next },
168
+ get stagePicked() { return stagePicked }, set stagePicked(next) { stagePicked = next },
169
+ get drill() { return drill },
170
+ get picker() { return picker }, set picker(next) { picker = next },
171
+ get souvenirPick() { return souvenirPick }, set souvenirPick(next) { souvenirPick = next },
172
+ get ownerEdit() { return ownerEdit }, set ownerEdit(next) { ownerEdit = next },
173
+ get pigNameEdit() { return pigNameEdit }, set pigNameEdit(next) { pigNameEdit = next },
174
+ get cardEdit() { return cardEdit }, set cardEdit(next) { cardEdit = next },
175
+ get isOpen() { return isOpen }, set isOpen(next) { isOpen = next },
176
+ get lastStage() { return lastStage }, set lastStage(next) { lastStage = next },
177
+ get lastPendingAt() { return lastPendingAt }, set lastPendingAt(next) { lastPendingAt = next },
178
+ get lastPendingId() { return lastPendingId }, set lastPendingId(next) { lastPendingId = next },
179
+ get userRight() { return userRight }, set userRight(next) { userRight = next },
180
+ get userBottom() { return userBottom }, set userBottom(next) { userBottom = next },
181
+ get busy() { return busy }, set busy(next) { busy = next },
182
+ get stopped() { return stopped }, set stopped(next) { stopped = next },
183
+ get devMode() { return devMode },
184
+ }
185
+ var layout = createLayout(ctx)
186
+ var panel = createPanel(ctx)
187
+ ctx.select = panel.select
188
+ ctx.setOpen = panel.setOpen
189
+ ctx.fitPanel = layout.fitPanel
190
+ ctx.paintBar = layout.paintBar
191
+ ctx.buildIcon = layout.buildIcon
192
+ ctx.clampPig = layout.clampPig
193
+ var render = panel.render, renderContent = panel.renderContent
194
+ var setOpen = panel.setOpen, select = panel.select
195
+ var fitPanel = layout.fitPanel, clampPig = layout.clampPig
196
+ var paintBar = layout.paintBar, buildIcon = layout.buildIcon
197
+ for (var t = 0; t < TABS.length; t += 1) buildIcon(TABS[t])
198
+ // `ui` was built before these existed; point it at the real ones now.
199
+ var io = createIo(ctx)
200
+ var send = io.send, refresh = io.refresh
201
+ ctx.send = send
202
+ ctx.render = render
203
+ ctx.renderContent = renderContent
204
+ ctx.send = send
205
+ ctx.renderContent = renderContent
206
+ ctx.setOpen = setOpen
207
+ ctx.fitPanel = fitPanel
208
+ ctx.flash = flash
209
+
210
+ // 日常气泡(签到/礼包)的点击只在这里绑一次;它压在猪上面,事件不能冒泡给
211
+ // 拖动和摸摸。
212
+ dailyHint.addEventListener('pointerdown', function (event) { event.stopPropagation() })
213
+ dailyHint.addEventListener('click', function (event) {
214
+ event.stopPropagation()
215
+ var action = dailyHint.getAttribute('data-action')
216
+ if (action !== null && action !== '') send(action)
217
+ })
218
+
219
+ // ---- open / close ----
220
+
221
+ /**
222
+ * Developer tab. Drives the pig into any state so a change can be looked at
223
+ * immediately instead of waiting days for it — and so the states that are
224
+ * hard to reach by playing (dying, the last illness stage, an elder pig)
225
+ * can be checked at all.
226
+ */
227
+
228
+ // ---- drag the pig; right-click it for the menu ----
229
+ var drag = null
230
+ scene.addEventListener('pointerdown', function (event) {
231
+ if (event.button !== 0) return
232
+ drag = {
233
+ x: event.clientX, y: event.clientY,
234
+ right: parseFloat(getComputedStyle(host).right) || 18,
235
+ bottom: parseFloat(getComputedStyle(host).bottom) || 18,
236
+ moved: false,
237
+ }
238
+ scene.setAttribute('data-dragging', 'true')
239
+ scene.setPointerCapture?.(event.pointerId)
240
+ })
241
+ scene.addEventListener('pointermove', function (event) {
242
+ if (drag === null) return
243
+ var dx = event.clientX - drag.x
244
+ var dy = event.clientY - drag.y
245
+ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) drag.moved = true
246
+ userRight = drag.right - dx
247
+ userBottom = drag.bottom - dy
248
+ clampPig()
249
+ // The panel rides along so it is never left behind off screen.
250
+ fitPanel()
251
+ })
252
+ function endDrag() {
253
+ if (drag === null) return false
254
+ var moved = drag.moved
255
+ drag = null
256
+ scene.removeAttribute('data-dragging')
257
+ clampPig()
258
+ writeStore(POSITION_KEY, JSON.stringify({ right: userRight, bottom: userBottom }))
259
+ fitPanel()
260
+ return moved
261
+ }
262
+ var boxPokes = 0
263
+
264
+ /**
265
+ * Poke the box. Three pokes and the piglet comes out — the count is what
266
+ * makes it feel like something is in there rather than a button that
267
+ * happens to be cardboard-shaped.
268
+ */
269
+ function pokeBox() {
270
+ boxPokes += 1
271
+ react('poke', 560)
272
+ if (boxPokes >= BOX_POKES_TO_OPEN) {
273
+ boxPokes = 0
274
+ host.removeAttribute('data-poke')
275
+ showBubble('哇——!', 1200)
276
+ burst(['✨', '🎉', '💨'], 6)
277
+ send('hatch')
278
+ return
279
+ }
280
+ host.setAttribute('data-poke', String(boxPokes))
281
+ burst(['💨'], 2)
282
+ showBubble(BOX_POKE_LINES[boxPokes - 1], 2200)
283
+ }
284
+
285
+ // Left click is a pat on the head. The menu is on the context menu, so a
286
+ // stray click can no longer open or close the panel by accident.
287
+ scene.addEventListener('pointerup', function () {
288
+ if (endDrag()) return
289
+ // An unhatched save is a box, whether or not one exists yet.
290
+ if (view.hatched !== true) {
291
+ pokeBox()
292
+ return
293
+ }
294
+ if (!view.dead) flash('pet')
295
+ })
296
+ scene.addEventListener('pointercancel', function () { endDrag() })
297
+ scene.addEventListener('contextmenu', function (event) {
298
+ event.preventDefault()
299
+ setOpen(!isOpen)
300
+ if (isOpen && view.pig !== null) flash('pet')
301
+ })
302
+
303
+ // ---- life ----
304
+ clampPig()
305
+ setOpen(isOpen)
306
+ render(view)
307
+ refresh()
308
+ pollTimer = window.setInterval(refresh, POLL_MS)
309
+
310
+ // B6: the pig greets the owner once per page load (the host decides
311
+ // whether it has been away long enough), then speaks up now and then.
312
+ var chatTimer = null
313
+ function scheduleChat() {
314
+ var minutes = IDLE_CHAT_MINUTES.min + Math.random() * (IDLE_CHAT_MINUTES.max - IDLE_CHAT_MINUTES.min)
315
+ chatTimer = window.setTimeout(function () {
316
+ if (!stopped && !busy && view.pig !== null) send('chat', { reason: 'idle' })
317
+ scheduleChat()
318
+ }, minutes * 60000)
319
+ }
320
+ var greetTimer = window.setTimeout(function () {
321
+ if (!stopped && view.pig !== null) send('chat', { reason: 'enter' })
322
+ }, GREET_DELAY_MS)
323
+ scheduleChat()
324
+ // Optional call: minimal test environments stub a window without listeners.
325
+ // Optional call: minimal test environments stub a window without listeners.
326
+ function onResize() {
327
+ clampPig()
328
+ fitPanel()
329
+ }
330
+ window.addEventListener?.('resize', onResize)
331
+
332
+ // ---- developer mode: Ctrl+Shift+D ----
333
+ function setDevMode(on) {
334
+ devMode = on === true
335
+ writeStore(DEV_KEY, devMode ? '1' : '0')
336
+ host.setAttribute('data-dev', devMode ? 'true' : 'false')
337
+ paintBar()
338
+ if (devMode) {
339
+ setOpen(true)
340
+ select('dev')
341
+ showBubble('🔧 开发者模式已开', 2000)
342
+ } else {
343
+ if (tab === 'dev') select('status')
344
+ showBubble('开发者模式已关', 1600)
345
+ }
346
+ }
347
+
348
+ devMode = readStore(DEV_KEY) === '1'
349
+ host.setAttribute('data-dev', devMode ? 'true' : 'false')
350
+ if (devMode) paintBar()
351
+
352
+ function onKeyDown(event) {
353
+ if (event.ctrlKey && event.shiftKey && (event.key === 'D' || event.key === 'd')) {
354
+ event.preventDefault()
355
+ setDevMode(!devMode)
356
+ }
357
+ }
358
+ window.addEventListener?.('keydown', onKeyDown)
359
+
360
+ // Also reachable from the console, for when the panel is off screen.
361
+ try {
362
+ /** @type {any} */ (window).dshPigDev = {
363
+ on: function () { setDevMode(true) },
364
+ off: function () { setDevMode(false) },
365
+ toggle: function () { setDevMode(!devMode) },
366
+ }
367
+ } catch (error) { /* frozen window */ }
368
+
369
+ function dispose() {
370
+ stopped = true
371
+ window.removeEventListener?.('resize', onResize)
372
+ // #11: the dev shortcut and the console handle outlived the pig, so a
373
+ // reload could toggle a panel that had already been disposed.
374
+ window.removeEventListener?.('keydown', onKeyDown)
375
+ try { delete (/** @type {any} */ (window)).dshPigDev } catch (error) { /* frozen window */ }
376
+ if (pollTimer !== null) window.clearInterval(pollTimer)
377
+ if (chatTimer !== null) window.clearTimeout(chatTimer)
378
+ window.clearTimeout(greetTimer)
379
+ fx.dispose()
380
+ pollTimer = null
381
+ host.remove()
382
+ style.remove()
383
+ font.remove()
384
+ }
385
+
386
+ return dispose
387
+ }
388
+
389
+ exports.name = 'dsh-piggy'
390
+ exports.apply = apply
391
+ return module.exports
392
+ },
393
+ })