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,319 @@
1
+ // @ts-check
2
+ /**
3
+ * 面板渲染:开关面板、切页签、把快照画到 DOM 上
4
+ *
5
+ * 只通过 ctx 读写外壳的状态与元素(getter/setter 转发),不直接碰全局。
6
+ * @module dsh-piggy/client/panel
7
+ */
8
+ import { syncPigArt } from './art.js'
9
+ import { DEV_TAB, OPEN_KEY, TABS, UPDATE_TAB } from './constants.js'
10
+ import { button, el } from './dom.js'
11
+ import { normalize } from './normalize.js'
12
+ import { writeStore } from './storage.js'
13
+ import { CSS } from './styles.js'
14
+ import { renderBagTab } from './tabs/bag.js'
15
+ import { renderCardTab } from './tabs/card.js'
16
+ import { renderCrownTab } from './tabs/crown.js'
17
+ import { appHeader, renderHome } from './tabs/home.js'
18
+ import { renderDevTab } from './tabs/dev.js'
19
+ import { renderUpdateTab, updatesBridge } from './tabs/update.js'
20
+ import { renderShopTab } from './tabs/shop.js'
21
+ import { renderStatusTab } from './tabs/status.js'
22
+ import { renderStudyTab } from './tabs/study.js'
23
+ import { renderTravelTab } from './tabs/travel.js'
24
+ import { renderWorkTab } from './tabs/work.js'
25
+ import { str } from './values.js'
26
+
27
+ /** News that gets through 免打扰. */
28
+ var URGENT_KINDS = ['sick', 'worse', 'death', 'cured', 'revived']
29
+
30
+ export function createPanel(ctx) {
31
+ var AWAY_LINE = {
32
+ work: '在忙',
33
+ study: '在念书',
34
+ trip: '在路上',
35
+ }
36
+
37
+ function setOpen(next) {
38
+ ctx.isOpen = next
39
+ ctx.host.setAttribute('data-open', next ? 'true' : 'false')
40
+ // Collapsed must be the pig and *nothing else*. One switch hides the
41
+ // whole panel now that the pig is not inside it — and driving visibility
42
+ // from the DOM rather than only from CSS makes it something a test can
43
+ // actually assert.
44
+ ctx.card.hidden = !next
45
+ // The hud rides with the panel: a bare pig in the corner should not have
46
+ // a name and a coin count floating beside it.
47
+ ctx.hud.hidden = !next
48
+ if (!next) ctx.bubble.hidden = true
49
+ writeStore(OPEN_KEY, next ? 'true' : 'false')
50
+ if (next) {
51
+ renderContent()
52
+ ctx.fitPanel()
53
+ } else {
54
+ // Back to the default anchor so the next open starts from a clean
55
+ // slate. `auto` (not '') keeps the stylesheet's bottom from re-applying
56
+ // alongside a stale top.
57
+ ctx.card.style.right = ''
58
+ ctx.card.style.top = 'auto'
59
+ ctx.card.style.bottom = ''
60
+ ctx.card.style.maxHeight = ''
61
+ ctx.card.style.maxWidth = ''
62
+ }
63
+ }
64
+
65
+ function select(next) {
66
+ ctx.tab = next
67
+ ctx.picker = null
68
+ // Opening a tile tab always starts at its top layer.
69
+ if (next in ctx.drill) { ctx.drill[next] = null; ctx.drill.pick = null }
70
+ renderContent()
71
+ for (var k in ctx.icons) ctx.icons[k].setAttribute('data-active', k === ctx.tab ? 'true' : 'false')
72
+ }
73
+
74
+ /**
75
+ * Repaint the panel body, keeping the reader's place.
76
+ *
77
+ * The body is rebuilt from scratch on every poll; without saving the
78
+ * offset, a long shelf jumped back to the top every four seconds.
79
+ */
80
+ function renderContent() {
81
+ var scrollTop = ctx.content.scrollTop
82
+ paintContent()
83
+ ctx.content.scrollTop = scrollTop
84
+ }
85
+
86
+ function paintContent() {
87
+ ctx.content.textContent = ''
88
+ for (var k = 0; k < TABS.length; k += 1) {
89
+ ctx.icons[TABS[k].key].setAttribute('data-active', TABS[k].key === ctx.tab ? 'true' : 'false')
90
+ }
91
+ if (ctx.host.getAttribute('data-open') !== 'true') return
92
+
93
+ // Only the host-version warning sits above every tab; the pig's own
94
+ // banners (away / sick / gone) live on the status tab (B9).
95
+ if (ctx.view.legacy) {
96
+ var legacy = el('div', 'dp-alert dp-legacy')
97
+ legacy.appendChild(el('b', null, '⚠️ 宿主是旧版本'))
98
+ legacy.appendChild(el('div', null, '金币、健康、打工、商店这些是新增的,重启 dsh(不是刷新页面)之后才会出现。'))
99
+ ctx.content.appendChild(legacy)
100
+ }
101
+ if (ctx.view.pig === null) {
102
+ ctx.content.appendChild(el('div', 'dp-empty', '门口放着一个纸盒,里面窸窸窣窣 📦'))
103
+ var grid = el('div', 'dp-actions')
104
+ var hatch = button('dp-btn dp-btn-wide', { 'data-action': 'hatch' }, function () { ctx.send('hatch') })
105
+ hatch.appendChild(el('span', null, '🥚'))
106
+ hatch.appendChild(el('span', null, '拆开纸盒'))
107
+ grid.appendChild(hatch)
108
+ ctx.content.appendChild(grid)
109
+ return
110
+ }
111
+
112
+ // B9: the home screen first; every app gets a 「‹」 back to it on its
113
+ // top layer (inside a category the app's own 「‹」 goes up a layer).
114
+ var apps = TABS.concat(updatesBridge() !== null ? [UPDATE_TAB] : [], ctx.devMode ? [DEV_TAB] : [])
115
+ if (ctx.tab === 'home') {
116
+ renderHome(ctx, apps)
117
+ ctx.fitPanel()
118
+ return
119
+ }
120
+ var drilled = ctx.tab in ctx.drill && ctx.drill[ctx.tab] !== null
121
+ var app = apps.find(function (entry) { return entry.key === ctx.tab })
122
+ if (app !== undefined && !drilled) appHeader(ctx, app, ctx.tab === 'shop' ? '🪙 ' + ctx.view.pig.coins : '')
123
+
124
+ if (ctx.tab === 'status') renderStatusTab(ctx)
125
+ else if (ctx.tab === 'card') renderCardTab(ctx)
126
+ else if (ctx.tab === 'crown') renderCrownTab(ctx)
127
+ else if (ctx.tab === 'study') renderStudyTab(ctx)
128
+ else if (ctx.tab === 'work') renderWorkTab(ctx)
129
+ else if (ctx.tab === 'shop') renderShopTab(ctx)
130
+ else if (ctx.tab === 'travel') renderTravelTab(ctx)
131
+ else if (ctx.tab === 'dev') renderDevTab(ctx)
132
+ else if (ctx.tab === 'update') renderUpdateTab(ctx)
133
+ else renderBagTab(ctx)
134
+
135
+ // Every tab is a different height, so the fit is recomputed after each
136
+ // render rather than only on open.
137
+ ctx.fitPanel()
138
+ }
139
+
140
+ function render(next) {
141
+ ctx.view = normalize(next)
142
+ // Never leave the study tab parked on a stage the pig cannot attend —
143
+ // but only until the user picks one themselves: after that the poll
144
+ // must not yank their choice away.
145
+ var stageEntry = null
146
+ var firstOpen = null
147
+ for (var s = 0; s < ctx.view.stages.length; s += 1) {
148
+ var entry = ctx.view.stages[s]
149
+ if (entry.unlocked !== false && firstOpen === null) firstOpen = entry.key
150
+ if (entry.key === ctx.stage) stageEntry = entry
151
+ }
152
+ // The 兴趣 button is not a stage; leave it selected.
153
+ if (!ctx.stagePicked && firstOpen !== null && (stageEntry === null || stageEntry.unlocked === false)) ctx.stage = firstOpen
154
+ ctx.host.setAttribute('data-dead', ctx.view.dead ? 'true' : 'false')
155
+ ctx.host.setAttribute('data-open', ctx.isOpen ? 'true' : 'false')
156
+ ctx.host.setAttribute('data-dev', ctx.devMode ? 'true' : 'false')
157
+ // Drives both the prop and the pig's own activity animation.
158
+ ctx.host.setAttribute('data-away', ctx.view.activity === null ? 'false' : ctx.view.activity.kind)
159
+ if (ctx.view.activity === null) {
160
+ ctx.work.hidden = true
161
+ } else {
162
+ ctx.work.hidden = false
163
+ ctx.prop.textContent = ctx.view.activity.emoji
164
+ ctx.progressFill.style.width = ctx.view.activity.progress + '%'
165
+ ctx.work.setAttribute('data-kind', ctx.view.activity.kind)
166
+ ctx.work.title = (AWAY_LINE[ctx.view.activity.kind] ?? '在外面') + ':' + ctx.view.activity.label
167
+ }
168
+
169
+ if (ctx.view.hatched !== true) {
170
+ ctx.pigArt.hidden = true
171
+ ctx.pigArt.removeAttribute('src')
172
+ ctx.pigEmoji.hidden = false
173
+ ctx.pigEmoji.textContent = ctx.view.boxStage.emoji
174
+ ctx.pig.removeAttribute('data-art')
175
+ ctx.pig.setAttribute('data-mood', 'box')
176
+ // Size comes from the host so the box and the pig can never drift.
177
+ ctx.host.style.setProperty('--pig-size', ctx.view.boxStage.size + 'px')
178
+ ctx.soul.hidden = true
179
+ ctx.host.setAttribute('data-soul', 'false')
180
+ ctx.host.setAttribute('data-faded', 'false')
181
+ ctx.host.setAttribute('data-unhatched', 'true')
182
+ ctx.pokeHint.hidden = false
183
+ ctx.hudName.textContent = '一个' + ctx.view.boxStage.label
184
+ ctx.hudCoins.textContent = '点开拆开它'
185
+ ctx.hudHealth.textContent = ''
186
+ ctx.lastStage = null
187
+ } else {
188
+ const pigStage = ctx.view.pig.stage
189
+ // A drawn stage shows its sprite; everything else is the emoji.
190
+ if (pigStage.art !== null) {
191
+ ctx.pigArt.hidden = false
192
+ ctx.pigEmoji.hidden = true
193
+ ctx.pig.setAttribute('data-art', pigStage.art)
194
+ ctx.pig.setAttribute('data-art-actions', pigStage.actionArt ? 'true' : 'false')
195
+ ctx.host.setAttribute('data-art-actions', pigStage.actionArt ? 'true' : 'false')
196
+ ctx.pig.setAttribute('data-activity', ctx.view.activity === null ? '' : ctx.view.activity.kind)
197
+ syncPigArt(ctx.pig, ctx.pigArt)
198
+ } else {
199
+ ctx.pigArt.hidden = true
200
+ ctx.pigArt.removeAttribute('src')
201
+ ctx.pigEmoji.hidden = false
202
+ ctx.pigEmoji.textContent = pigStage.emoji
203
+ ctx.pig.removeAttribute('data-art')
204
+ ctx.pig.removeAttribute('data-art-actions')
205
+ ctx.host.removeAttribute('data-art-actions')
206
+ }
207
+ // Literally grows up: the stage carries its own size.
208
+ ctx.host.style.setProperty('--pig-size', pigStage.size + 'px')
209
+ ctx.pig.setAttribute('data-mood', ctx.view.pig.mood)
210
+ ctx.host.setAttribute('data-soul', ctx.view.pig.soul ? 'true' : 'false')
211
+ // Old age reads as a faded coat, since every stage is the same pig.
212
+ ctx.host.setAttribute('data-faded', pigStage.faded ? 'true' : 'false')
213
+ ctx.host.setAttribute('data-unhatched', 'false')
214
+ ctx.pokeHint.hidden = true
215
+ ctx.soul.hidden = ctx.view.pig.soul !== true
216
+ ctx.pig.setAttribute('data-stage', pigStage.key)
217
+ // 装扮挂在猪身上(见 .dp-slot),名字牌上不再重复一遍。
218
+ ctx.dressSlots.textContent = ''
219
+ for (var wd = 0; wd < ctx.view.dress.length; wd += 1) {
220
+ var piece = ctx.view.dress[wd]
221
+ if (!piece.worn || piece.slot === '') continue
222
+ // 加冕后的样子自带王冠披风,盖住的位置不挂装扮(东西还在背包里)。
223
+ if (pigStage.hides.indexOf(piece.slot) >= 0) continue
224
+ var node = el('span', 'dp-slot', piece.emoji)
225
+ node.setAttribute('data-slot', piece.slot)
226
+ ctx.dressSlots.appendChild(node)
227
+ }
228
+ ctx.hudName.textContent = ctx.view.pig.name
229
+ + (ctx.view.pig.sex !== null ? ' ' + ctx.view.pig.sex.symbol : '')
230
+ + ' Lv.' + ctx.view.pig.level.level
231
+ + ' · ' + pigStage.label
232
+ + (ctx.view.pig.ageLabel ? ' · ' + ctx.view.pig.ageLabel : '')
233
+ + (ctx.view.pig.ageForced ? ' 🔧' : '')
234
+ ctx.hudCoins.textContent = '🪙 ' + ctx.view.pig.coins
235
+ ctx.hudHealth.textContent = '💚 ' + ctx.view.pig.health + '/' + ctx.view.maxHealth
236
+ // Growing up is announced with the same flourish a level-up used to get.
237
+ if (ctx.lastStage !== null && pigStage.key !== ctx.lastStage) {
238
+ ctx.react('levelup', 950)
239
+ ctx.burst(['✨', '🎉', '⭐'], 4)
240
+ ctx.showBubble('我长大啦!' + pigStage.emoji, 2600)
241
+ }
242
+ ctx.lastStage = pigStage.key
243
+ }
244
+
245
+ // 猪头上的日常提示:能签到就先显示签到,否则显示礼包。
246
+ // 点一下直接领;点击不再冒泡到场景,免得同时被当成摸猪/拖动。
247
+ var daily = ctx.view.daily
248
+ var dailyAction = daily.canSignIn ? 'signIn' : (daily.unclaimed > 0 ? 'openGift' : null)
249
+ ctx.dailyHint.hidden = dailyAction === null || ctx.view.pig === null
250
+ if (dailyAction !== null) {
251
+ ctx.dailyHint.textContent = dailyAction === 'signIn' ? '📅' : '🎁'
252
+ ctx.dailyHint.title = dailyAction === 'signIn'
253
+ ? '签到第 ' + daily.signInDay + '/' + daily.cycle + ' 天'
254
+ : '有 ' + daily.unclaimed + ' 个在线礼包'
255
+ // 动作放在 data-action 上,监听只在外壳里注册一次(见 index.js),
256
+ // 免得每 4 秒重绘都往上挂一个 listener。
257
+ ctx.dailyHint.setAttribute('data-action', dailyAction)
258
+ }
259
+
260
+ // Alerts on the icon bar itself, so a collapsed pig still warns.
261
+ // The study icon lights when there is a course to take right now:
262
+ // idle, the stage on screen is unlocked, and it still has a subject.
263
+ var studyStage = null
264
+ for (var st = 0; st < ctx.view.stages.length; st += 1) {
265
+ if (ctx.view.stages[st].key === ctx.stage) studyStage = ctx.view.stages[st]
266
+ }
267
+ var studyOpen = ctx.view.canGoOut && (studyStage === null || studyStage.unlocked !== false)
268
+ // B4: no stage ladder any more, so "a course to take" is simply one the
269
+ // pig can afford (older hosts still send stages, handled above).
270
+ var hasCourse = studyOpen && ctx.view.subjects.some(function (subject) {
271
+ var onStage = studyStage === null || studyStage.subjects.length === 0 || studyStage.subjects.indexOf(subject.key) >= 0
272
+ return onStage && subject.affordable
273
+ })
274
+ ctx.icons.study.setAttribute('data-alert', hasCourse ? 'true' : 'false')
275
+ ctx.icons.shop.setAttribute('data-alert', ctx.view.pig !== null && ctx.view.pig.illness !== null ? 'true' : 'false')
276
+ // 加冕: a form is waiting to be picked.
277
+ ctx.icons.crown.setAttribute('data-alert', ctx.view.forms !== null && ctx.view.forms.forms.some(function (f) { return f.ready }) ? 'true' : 'false')
278
+ ctx.icons.travel.setAttribute('data-alert', ctx.view.pig !== null && ctx.view.pig.coins >= 400 ? 'true' : 'false')
279
+
280
+ for (var i = 0; i < ctx.view.pending.length; i += 1) {
281
+ var event = ctx.view.pending[i]
282
+ // Messages carry an id that only goes up; two from the same instant
283
+ // ("病情加重" then "走了") used to collapse into the first one.
284
+ // Hosts older than the id still dedupe on the timestamp.
285
+ if (event.id > 0 ? event.id <= ctx.lastPendingId : event.at <= ctx.lastPendingAt) continue
286
+ if (event.id > 0) ctx.lastPendingId = event.id
287
+ ctx.lastPendingAt = Math.max(ctx.lastPendingAt, event.at)
288
+ if (event.kind === 'line') {
289
+ showPigLine(event)
290
+ continue
291
+ }
292
+ // 免打扰: routine news stays quiet; illness and death still speak.
293
+ if (ctx.view.dialogue.quiet && URGENT_KINDS.indexOf(event.kind) < 0) continue
294
+ ctx.toast(str(event.text, '猪有新消息'))
295
+ if (event.kind === 'coronation') { ctx.react('levelup', 950); ctx.burst(['👑', '✨'], 3) }
296
+ else if (event.kind === 'levelup') { ctx.react('levelup', 950); ctx.burst(['✨', '🎉'], 3) }
297
+ else if (event.kind === 'cured') { ctx.react('cure', 900); ctx.burst(['💚', '✨'], 3) }
298
+ else if (event.kind === 'death') ctx.react('refuse', 700)
299
+ else if (event.kind === 'work') { ctx.react('away', 900); ctx.burst(['🪙', '💰'], 3) }
300
+ else if (event.kind === 'study') { ctx.react('away', 900); ctx.burst(['📚', '✨'], 3) }
301
+ else if (event.kind === 'trip') { ctx.react('away', 900); ctx.burst(['🧳', '🎁'], 3) }
302
+ }
303
+
304
+ // Typing a new name: a repaint would drop the input and its focus.
305
+ if ((ctx.ownerEdit !== null || ctx.pigNameEdit !== null) && ctx.tab === 'status') return
306
+ if (ctx.cardEdit !== null && ctx.tab === 'card') return
307
+ renderContent()
308
+ }
309
+
310
+ /** A line from the pig goes in its bubble, with the owner's reply buttons under it. */
311
+ function showPigLine(event) {
312
+ var lineId = event.id
313
+ ctx.showLine(event.text, event.replies, function (index) {
314
+ ctx.send('reply', { line: lineId, index: index })
315
+ })
316
+ }
317
+
318
+ return { setOpen, select, renderContent, render }
319
+ }
@@ -0,0 +1,113 @@
1
+ // @ts-check
2
+ /**
3
+ * 场景搭建:注入样式与字体,建出猪、气泡、进度条、图标栏与面板骨架。
4
+ *
5
+ * 只创建 DOM,不绑定事件、不读状态(见 docs/CONVENTIONS.md);
6
+ * 事件与渲染由 shell 负责。
7
+ * @module dsh-piggy/client/scene
8
+ */
9
+ import { MOUNTED } from './constants.js'
10
+ import { el } from './dom.js'
11
+ import { CSS } from './styles.js'
12
+
13
+ /** Build the widget once and hand every element back to the shell. */
14
+ export function createScene() {
15
+ var font = document.createElement('link')
16
+ font.rel = 'stylesheet'
17
+ font.href = 'https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900'
18
+ + '&family=Noto+Sans+SC:wght@400;500;700&display=swap'
19
+ document.head.appendChild(font)
20
+
21
+ var style = document.createElement('style')
22
+ style.textContent = CSS
23
+ document.head.appendChild(style)
24
+
25
+ var host = document.createElement('div')
26
+ host.setAttribute(MOUNTED, '')
27
+
28
+ var card = el('div', 'dp-card')
29
+ // The pig lives beside the panel, not inside it, so it stays transparent
30
+ // and unmoved when the panel opens.
31
+ var scene = el('div', 'dp-scene')
32
+
33
+ var hud = el('div', 'dp-hud')
34
+ var hudName = el('div', null, '猪猪')
35
+ var hudCoins = el('div', null, '🪙 0')
36
+ var hudHealth = el('div', null, '💚 5/5')
37
+ hud.appendChild(hudName)
38
+ hud.appendChild(hudCoins)
39
+ hud.appendChild(hudHealth)
40
+ scene.appendChild(hud)
41
+
42
+ var bubble = el('div', 'dp-bubble', '')
43
+ bubble.hidden = true
44
+ scene.appendChild(bubble)
45
+
46
+ // What the pig is doing while it is out: a prop to work/read/travel with,
47
+ // and a line showing how far through it is. Sits to the pig's left, so the
48
+ // pig itself never shifts when it appears.
49
+ var work = el('div', 'dp-work')
50
+ var prop = el('span', 'dp-prop', '💼')
51
+ var progressWrap = el('div', 'dp-progress')
52
+ var progressFill = document.createElement('i')
53
+ progressWrap.appendChild(progressFill)
54
+ work.appendChild(prop)
55
+ work.appendChild(progressWrap)
56
+ work.hidden = true
57
+ scene.appendChild(work)
58
+
59
+ // Shown while the box is still shut, so it reads as something to poke
60
+ // rather than a decorative cardboard box sitting in the corner.
61
+ var pokeHint = el('div', 'dp-poke-hint')
62
+ pokeHint.appendChild(el('span', null, '👆'))
63
+ pokeHint.appendChild(el('span', null, '戳三下'))
64
+ pokeHint.hidden = true
65
+ scene.appendChild(pokeHint)
66
+
67
+ // 签到 / 在线礼包的小气泡,位置在猪头上,样式在 css-tabs.js。
68
+ var dailyHint = el('button', 'dp-daily')
69
+ dailyHint.hidden = true
70
+ scene.appendChild(dailyHint)
71
+
72
+ var soul = el('span', 'dp-soul', '👻')
73
+ soul.hidden = true
74
+ scene.appendChild(soul)
75
+
76
+ // Drawn stages (the piglet, the elder pig) use an <img>; the rest fall
77
+ // back to the emoji. Both live in the pig box so the layout never cares.
78
+ var pigArt = document.createElement('img')
79
+ pigArt.className = 'dp-pig-img'
80
+ pigArt.alt = ''
81
+ pigArt.hidden = true
82
+ var pigEmoji = el('span', 'dp-pig-emoji', '🐖')
83
+ var pig = el('div', 'dp-pig')
84
+ pig.appendChild(pigArt)
85
+ pig.appendChild(pigEmoji)
86
+ // 装扮点位:每个点位挂一件,位置全在 CSS 里(.dp-slot[data-slot=…])。
87
+ var dressSlots = el('div', 'dp-dress')
88
+ pig.appendChild(dressSlots)
89
+ scene.appendChild(pig)
90
+ // Right-click is not discoverable on its own, so the native tooltip says so.
91
+ scene.title = '左键摸摸 · 右键打开面板 · 拖动可移动'
92
+
93
+ var bar = el('div', 'dp-bar')
94
+
95
+ var content = el('div', 'dp-content')
96
+
97
+ // Panel first, pig second: as flex siblings in a bottom-anchored column,
98
+ // the pig ends up at a fixed screen position whether the panel is open or
99
+ // not, and the panel can only ever grow upwards from it.
100
+ card.appendChild(content)
101
+ card.appendChild(bar)
102
+ host.appendChild(card)
103
+ host.appendChild(scene)
104
+ if (document.body !== null && document.body !== undefined) {
105
+ document.body.appendChild(host)
106
+ } else {
107
+ document.addEventListener('DOMContentLoaded', function () {
108
+ try { document.body.appendChild(host) } catch (error) { /* shell not ready */ }
109
+ }, { once: true })
110
+ }
111
+
112
+ return { font, style, host, card, scene, hud, hudName, hudCoins, hudHealth, bubble, work, prop, progressWrap, progressFill, pokeHint, dailyHint, soul, pigArt, pigEmoji, pig, dressSlots, bar, content }
113
+ }
@@ -0,0 +1,19 @@
1
+ // @ts-check
2
+ /**
3
+ * 面板自己的小存储:展开状态、位置与开发者模式开关。
4
+ *
5
+ * 隐私模式下 localStorage 会抛异常,所以读写都要兜住。
6
+ * @module dsh-piggy/client/storage
7
+ */
8
+ export function readStore(key) {
9
+ try {
10
+ var value = window.localStorage.getItem(key)
11
+ // Keys were 'dsh-pig:*' before the rename; read those once so nothing resets.
12
+ return value !== null ? value : window.localStorage.getItem(key.replace(/^dsh-piggy:/, 'dsh-pig:'))
13
+ } catch (error) { return null }
14
+ }
15
+
16
+
17
+ export function writeStore(key, value) {
18
+ try { window.localStorage.setItem(key, value) } catch (error) { /* private mode */ }
19
+ }
@@ -0,0 +1,13 @@
1
+ // @ts-check
2
+ /**
3
+ * 样式入口:基础样式 + 页签样式拼成一张表。
4
+ *
5
+ * @module dsh-piggy/client/styles
6
+ */
7
+ import { CSS_BASE } from './css-base.js'
8
+ import { CSS_TABS } from './css-tabs.js'
9
+ import { CSS_TILES } from './css-tiles.js'
10
+ import { CSS_CARD } from './css-card.js'
11
+
12
+ /** The whole stylesheet, in the order it must be applied. */
13
+ export const CSS = CSS_BASE + CSS_TABS + CSS_TILES + CSS_CARD
@@ -0,0 +1,203 @@
1
+ // @ts-check
2
+ /**
3
+ * 背包页签(B8:动森手机式方块,跟商店一个样子)。
4
+ *
5
+ * 第一层:食物 / 洗浴 / 玩具 / 药品 / 复活 / 装扮 / 日记 / 纪念品,右上角写件数,空的变灰。
6
+ * 第二层:道具点一下就用;装扮点一下穿上或脱下;日记和纪念品点一下,下面显示全文 / 故事。
7
+ * @module dsh-piggy/client/tabs/bag
8
+ */
9
+
10
+ import { KIND_ORDER } from '../constants.js'
11
+ import { button, el } from '../dom.js'
12
+ import { num } from '../values.js'
13
+ import { drillHeader, drillTo, tile, tileGrid } from '../widgets.js'
14
+ import { SHELF_COLOR, shelfParts } from './shop.js'
15
+
16
+ /** The consumable shelves, in shop order; 装扮 is its own category below. */
17
+ var CONSUMABLES = KIND_ORDER.filter(function (kind) { return kind !== 'dress' })
18
+
19
+ /** The non-shelf categories: what they are called and how they look. */
20
+ var EXTRA = {
21
+ dress: { emoji: '👕', label: '装扮', color: 'pink' },
22
+ diary: { emoji: '📔', label: '日记', color: 'brown' },
23
+ souvenir: { emoji: '🎁', label: '纪念品', color: 'blue' },
24
+ }
25
+
26
+ /** 「2026-10-01」 → 「10-01」: the tile only has room for the month and day. */
27
+ function shortDay(day) {
28
+ return day.length >= 10 ? day.slice(5) : day
29
+ }
30
+
31
+ export function renderBagTab(ui) {
32
+ var open = ui.drill.bag
33
+ if (open === 'dress') renderDress(ui)
34
+ else if (open === 'diary') renderDiary(ui)
35
+ else if (open === 'souvenir') renderSouvenirs(ui)
36
+ else if (open !== null && CONSUMABLES.indexOf(open) >= 0) renderItems(ui, open)
37
+ else renderCategories(ui)
38
+ }
39
+
40
+ /** What the pig owns of one shelf, with counts. */
41
+ function ownedOf(ui, kind) {
42
+ return ui.view.shop.filter(function (item) {
43
+ return item.kind === kind && num(ui.view.inventory[item.key], 0) > 0
44
+ })
45
+ }
46
+
47
+ function renderCategories(ui) {
48
+ var grid = tileGrid()
49
+ for (var k = 0; k < CONSUMABLES.length; k += 1) {
50
+ (function (kind) {
51
+ var items = ownedOf(ui, kind)
52
+ var count = items.reduce(function (sum, item) { return sum + num(ui.view.inventory[item.key], 0) }, 0)
53
+ var parts = shelfParts(kind)
54
+ grid.appendChild(tile({
55
+ emoji: parts[0], label: parts[1], color: SHELF_COLOR[kind] ?? 'blue',
56
+ badge: count > 0 ? String(count) : '', dim: count === 0,
57
+ tag: items.some(function (item) { return item.needed }) ? '需要' : '',
58
+ data: { 'data-bag': kind },
59
+ onPick: function () { drillTo(ui, 'bag', kind) },
60
+ }))
61
+ })(CONSUMABLES[k])
62
+ }
63
+ var counts = {
64
+ dress: ui.view.dress.filter(function (item) { return item.owned }).length,
65
+ diary: ui.view.diary.length,
66
+ souvenir: ui.view.pig.souvenirs.length,
67
+ }
68
+ for (var key in EXTRA) {
69
+ (function (category) {
70
+ var spec = EXTRA[category]
71
+ grid.appendChild(tile({
72
+ emoji: spec.emoji, label: spec.label, color: spec.color,
73
+ badge: counts[category] > 0 ? String(counts[category]) : '', dim: counts[category] === 0,
74
+ data: { 'data-bag': category },
75
+ onPick: function () { drillTo(ui, 'bag', category) },
76
+ }))
77
+ })(key)
78
+ }
79
+ ui.content.appendChild(grid)
80
+ }
81
+
82
+ /** A consumable shelf: tap an item to use it. */
83
+ function renderItems(ui, kind) {
84
+ var parts = shelfParts(kind)
85
+ drillHeader(ui, 'bag', parts[0] + ' ' + parts[1], '点一下就用')
86
+ var items = ownedOf(ui, kind)
87
+ if (items.length === 0) {
88
+ ui.content.appendChild(el('div', 'dp-empty', '空的'))
89
+ return
90
+ }
91
+ var grid = tileGrid()
92
+ for (var i = 0; i < items.length; i += 1) {
93
+ (function (item) {
94
+ grid.appendChild(tile({
95
+ emoji: item.emoji, label: item.label, color: SHELF_COLOR[kind] ?? 'blue', soft: true,
96
+ badge: '×' + num(ui.view.inventory[item.key], 0), tag: item.needed ? '需要' : '',
97
+ data: { 'data-use': item.key },
98
+ onPick: function () { ui.send('use', { item: item.key }) },
99
+ }))
100
+ })(items[i])
101
+ }
102
+ ui.content.appendChild(grid)
103
+ }
104
+
105
+ /** 家当: tap to put it on or take it off; one piece per slot. */
106
+ function renderDress(ui) {
107
+ var owned = ui.view.dress.filter(function (item) { return item.owned })
108
+ var worn = owned.filter(function (item) { return item.worn }).length
109
+ drillHeader(ui, 'bag', '👕 装扮', worn + ' 件穿着')
110
+ if (owned.length === 0) {
111
+ ui.content.appendChild(el('div', 'dp-empty', '还没有装扮'))
112
+ return
113
+ }
114
+ var grid = tileGrid()
115
+ for (var i = 0; i < owned.length; i += 1) {
116
+ (function (item) {
117
+ grid.appendChild(tile({
118
+ emoji: item.emoji, label: item.label, color: EXTRA.dress.color, soft: true,
119
+ note: item.slotLabel, tag: item.worn ? '穿着' : '', active: item.worn,
120
+ data: { 'data-wear': item.key },
121
+ onPick: function () { ui.send('wear', { item: item.key, on: !item.worn }) },
122
+ }))
123
+ })(owned[i])
124
+ }
125
+ ui.content.appendChild(grid)
126
+ }
127
+
128
+ /** 日记: one tile per day, newest first; the picked day's page opens below. */
129
+ function renderDiary(ui) {
130
+ drillHeader(ui, 'bag', '📔 日记', ui.view.diary.length + ' 篇')
131
+ if (ui.view.diary.length === 0) {
132
+ ui.content.appendChild(el('div', 'dp-empty', '还没有日记'))
133
+ return
134
+ }
135
+ var grid = tileGrid()
136
+ var picked = null
137
+ for (var d = 0; d < ui.view.diary.length; d += 1) {
138
+ (function (entry) {
139
+ var active = ui.drill.pick === entry.day
140
+ if (active) picked = entry
141
+ grid.appendChild(tile({
142
+ emoji: '📔', label: shortDay(entry.day), color: EXTRA.diary.color, soft: true, active: active,
143
+ data: { 'data-diary': entry.day },
144
+ onPick: function () {
145
+ ui.drill.pick = active ? null : entry.day
146
+ ui.renderContent()
147
+ },
148
+ }))
149
+ })(ui.view.diary[d])
150
+ }
151
+ ui.content.appendChild(grid)
152
+ if (picked !== null) {
153
+ var page = el('div', 'dp-pick dp-tile-card dp-diary-page')
154
+ page.appendChild(el('div', 'dp-pick-head', picked.day))
155
+ page.appendChild(el('div', null, picked.text))
156
+ ui.content.appendChild(page)
157
+ }
158
+ }
159
+
160
+ /** 纪念品: tap one for its story, and sell it from there. */
161
+ function renderSouvenirs(ui) {
162
+ var list = ui.view.pig.souvenirs
163
+ drillHeader(ui, 'bag', '🎁 纪念品', list.length + ' 件')
164
+ if (list.length === 0) {
165
+ ui.content.appendChild(el('div', 'dp-empty', '收藏册还空着'))
166
+ return
167
+ }
168
+ var grid = tileGrid()
169
+ var picked = null
170
+ for (var s = 0; s < list.length; s += 1) {
171
+ (function (entry, index) {
172
+ var id = entry.key + '#' + index
173
+ var active = ui.drill.pick === id
174
+ if (active) picked = entry
175
+ grid.appendChild(tile({
176
+ emoji: entry.emoji, label: entry.label, color: EXTRA.souvenir.color, soft: true, active: active,
177
+ note: entry.rarityEmoji + entry.rarityLabel,
178
+ data: { 'data-souvenir': id },
179
+ onPick: function () {
180
+ ui.drill.pick = active ? null : id
181
+ ui.renderContent()
182
+ },
183
+ }))
184
+ })(list[s], s)
185
+ }
186
+ ui.content.appendChild(grid)
187
+ if (picked === null) return
188
+ var story = el('div', 'dp-pick dp-tile-card')
189
+ story.appendChild(el('div', 'dp-pick-head', picked.emoji + ' ' + picked.label
190
+ + (picked.fromLabel === '' ? '' : ' · ' + picked.fromLabel)))
191
+ story.appendChild(el('div', null, picked.story === '' ? '(旧版本带回来的,没有故事)' : '「' + picked.story + '」'))
192
+ if (picked.price > 0) {
193
+ var sold = picked
194
+ var sell = button('dp-mini', { 'data-sell': sold.key }, function () {
195
+ ui.drill.pick = null
196
+ ui.send('sell', { souvenir: sold.key })
197
+ })
198
+ sell.textContent = '卖掉 +' + sold.price + ' 🪙'
199
+ sell.style.marginTop = '6px'
200
+ story.appendChild(sell)
201
+ }
202
+ ui.content.appendChild(story)
203
+ }