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,128 @@
1
+ // @ts-check
2
+ /**
3
+ * 学习页签(B8:动森手机式方块)。
4
+ *
5
+ * 第一层:学段方块(小学 … 学无止境)+ 兴趣。锁住的学段变灰,但点得进去看。
6
+ * 第二层:学段里是九门课的方块,点一下就去上这一节;兴趣里是兴趣课方块,点一下就去学。
7
+ * B4 起每门课各算各的课时:同一个学段里,正在这一段的能上,念完的打勾,还没到的上锁。
8
+ * @module dsh-piggy/client/tabs/study
9
+ */
10
+
11
+ import { STAGES } from '../constants.js'
12
+ import { el } from '../dom.js'
13
+ import { drillHeader, drillTo, tile, tileGrid } from '../widgets.js'
14
+
15
+ /** The drill key that opens the interest courses instead of a stage. */
16
+ export const INTEREST_TAB = 'interest'
17
+
18
+ /** One colour per stage, so the inner layer still says which stage it is. */
19
+ var STAGE_COLOR = { primary: 'yellow', middle: 'teal', college: 'blue', graduate: 'purple', beyond: 'pink' }
20
+ var INTEREST_COLOR = 'orange'
21
+ /** Stages this client does not know (an older host's ladder) take colours in turn. */
22
+ var FALLBACK_COLORS = ['yellow', 'teal', 'blue', 'purple', 'pink', 'green', 'lime']
23
+
24
+ /** Where a subject stands relative to one stage: 'current' | 'done' | 'ahead'. */
25
+ function standing(sub, stage) {
26
+ if (stage === null || sub.stageKey === '') return 'current'
27
+ if (sub.stageKey === stage.key) return 'current'
28
+ if (stage.upTo !== null && sub.lessons >= stage.upTo) return 'done'
29
+ return 'ahead'
30
+ }
31
+
32
+ export function renderStudyTab(ui) {
33
+ if (ui.view.subjects.length === 0) {
34
+ ui.content.appendChild(el('div', 'dp-empty', '宿主还没提供课程表。'))
35
+ return
36
+ }
37
+ var open = ui.drill.study
38
+ if (open === INTEREST_TAB && ui.view.interests.length > 0) {
39
+ renderInterests(ui)
40
+ return
41
+ }
42
+ var stage = null
43
+ for (var d = 0; d < ui.view.stages.length; d += 1) if (ui.view.stages[d].key === open) stage = ui.view.stages[d]
44
+ if (stage === null) {
45
+ renderStages(ui)
46
+ return
47
+ }
48
+ renderSubjects(ui, stage)
49
+ }
50
+
51
+ /** The top layer: one tile per stage, then 兴趣. */
52
+ function renderStages(ui) {
53
+ // Prefer the host's own ladder: a client that hard-codes the stages would
54
+ // keep offering one the host has never heard of.
55
+ var stageList = ui.view.stages.length > 0 ? ui.view.stages : STAGES
56
+ var grid = tileGrid()
57
+ for (var s = 0; s < stageList.length; s += 1) {
58
+ (function (entry, index) {
59
+ var locked = entry.unlocked === false
60
+ var finished = entry.upTo === null || entry.upTo === undefined ? 0
61
+ : ui.view.subjects.filter(function (sub) { return sub.lessons >= entry.upTo }).length
62
+ grid.appendChild(tile({
63
+ emoji: entry.emoji || '📚', label: entry.label,
64
+ color: STAGE_COLOR[entry.key] ?? FALLBACK_COLORS[index % FALLBACK_COLORS.length],
65
+ locked: locked, tag: locked ? '🔒' : '', badge: finished > 0 ? '✓' + finished : '',
66
+ data: { 'data-stage': entry.key },
67
+ onPick: function () { drillTo(ui, 'study', entry.key) },
68
+ }))
69
+ })(stageList[s], s)
70
+ }
71
+ if (ui.view.interests.length > 0) {
72
+ var certified = ui.view.interests.filter(function (entry) { return entry.certified }).length
73
+ grid.appendChild(tile({
74
+ emoji: '🎯', label: '兴趣', color: INTEREST_COLOR,
75
+ badge: certified > 0 ? '📜' + certified : '',
76
+ data: { 'data-stage': INTEREST_TAB },
77
+ onPick: function () { drillTo(ui, 'study', INTEREST_TAB) },
78
+ }))
79
+ }
80
+ ui.content.appendChild(grid)
81
+ }
82
+
83
+ /** Inside a stage: the nine subjects. Tap one that is at this stage to go to class. */
84
+ function renderSubjects(ui, stage) {
85
+ drillHeader(ui, 'study', stage.emoji + ' ' + stage.label,
86
+ stage.minutes + ' 分钟 · ' + stage.tuition + ' 🪙 · +' + stage.gain)
87
+ var color = STAGE_COLOR[stage.key] ?? FALLBACK_COLORS[Math.max(0, ui.view.stages.indexOf(stage)) % FALLBACK_COLORS.length]
88
+ var grid = tileGrid()
89
+ for (var i = 0; i < ui.view.subjects.length; i += 1) {
90
+ (function (sub) {
91
+ var where = standing(sub, stage)
92
+ var note
93
+ if (where === 'done') note = '✓ 毕业'
94
+ else if (where === 'ahead') note = '🔒 ' + (sub.stageLabel || '没到')
95
+ else if (stage.upTo !== null) note = (sub.lessons - stage.from) + '/' + (stage.upTo - stage.from) + ' 节'
96
+ else note = sub.lessons + ' 节'
97
+ grid.appendChild(tile({
98
+ emoji: sub.emoji, label: sub.label, color: color, soft: true, note: note,
99
+ disabled: where !== 'current' || !ui.view.canGoOut,
100
+ dim: where === 'current' && !sub.affordable,
101
+ data: { 'data-subject': sub.key },
102
+ onPick: function () { ui.send('study', { subject: sub.key }) },
103
+ }))
104
+ })(ui.view.subjects[i])
105
+ }
106
+ ui.content.appendChild(grid)
107
+ }
108
+
109
+ /** Inside 兴趣: every interest course; tap to go. Five of one earns its certificate. */
110
+ function renderInterests(ui) {
111
+ var after = ui.view.interests[0].certificateAfter
112
+ drillHeader(ui, 'study', '🎯 兴趣', after > 0 ? '上满 ' + after + ' 次拿证' : '')
113
+ var grid = tileGrid()
114
+ for (var n = 0; n < ui.view.interests.length; n += 1) {
115
+ (function (entry) {
116
+ var note = entry.certificate === '' ? entry.cost + ' 🪙'
117
+ : (entry.certified ? '📜 有证' : '📜 ' + entry.times + '/' + entry.certificateAfter)
118
+ grid.appendChild(tile({
119
+ emoji: entry.emoji, label: entry.label, color: INTEREST_COLOR, soft: true, note: note,
120
+ disabled: !ui.view.canGoOut,
121
+ dim: !entry.affordable,
122
+ data: { 'data-interest': entry.key },
123
+ onPick: function () { ui.send('interest', { interest: entry.key }) },
124
+ }))
125
+ })(ui.view.interests[n])
126
+ }
127
+ ui.content.appendChild(grid)
128
+ }
@@ -0,0 +1,89 @@
1
+ // @ts-check
2
+ /**
3
+ * 旅行页签。
4
+ *
5
+ * 目的地与纪念品收藏。
6
+ * @module dsh-piggy/client/tabs/travel
7
+ */
8
+
9
+ import { button, el } from '../dom.js'
10
+ import { formatMinutes } from '../format.js'
11
+ import { CSS } from '../styles.js'
12
+
13
+ export function renderTravelTab(ui) {
14
+ if (ui.view.trips.length === 0) {
15
+ ui.content.appendChild(el('div', 'dp-empty', '宿主还没提供目的地。'))
16
+ return
17
+ }
18
+ var list = el('div', 'dp-list')
19
+ for (var i = 0; i < ui.view.trips.length; i += 1) {
20
+ (function (trip) {
21
+ var row = el('div', 'dp-item')
22
+ row.appendChild(el('span', null, trip.emoji))
23
+ var grow = el('div', 'dp-grow')
24
+ grow.appendChild(el('div', null, trip.label))
25
+ grow.appendChild(el('div', 'dp-dim', formatMinutes(trip.minutes) + ' · ' + trip.cost + ' 🪙'
26
+ + (trip.bestRarity ? ' · 可带回 ' + trip.bestRarityEmoji + trip.bestRarity : '')))
27
+ row.appendChild(grow)
28
+ var go = button('dp-mini', { 'data-trip': trip.key }, function () { ui.send('trip', { trip: trip.key }) })
29
+ go.textContent = '出发'
30
+ go.disabled = !ui.view.canGoOut || !trip.affordable
31
+ row.appendChild(go)
32
+ list.appendChild(row)
33
+ })(ui.view.trips[i])
34
+ }
35
+ ui.content.appendChild(list)
36
+
37
+ var souvenirs = ui.view.pig.souvenirs
38
+ var head = el('div', 'dp-title')
39
+ head.style.marginTop = '10px'
40
+ head.appendChild(el('b', null, '🎁 纪念品 ' + souvenirs.length))
41
+ ui.content.appendChild(head)
42
+ if (souvenirs.length === 0) {
43
+ ui.content.appendChild(el('div', 'dp-empty', '还没出过远门。'))
44
+ return
45
+ }
46
+
47
+ var chips = el('div', 'dp-grid')
48
+ for (var s = 0; s < souvenirs.length; s += 1) {
49
+ (function (entry) {
50
+ var chip = button('dp-item', { 'data-souvenir': entry.key }, function () {
51
+ ui.souvenirPick = ui.souvenirPick === entry.key ? null : entry.key
52
+ ui.renderContent()
53
+ })
54
+ chip.appendChild(el('span', null, entry.emoji))
55
+ var grow = el('div', 'dp-grow')
56
+ grow.appendChild(el('div', null, entry.label))
57
+ grow.appendChild(el('div', 'dp-dim', entry.rarityEmoji + entry.rarityLabel
58
+ + (entry.price > 0 ? ' · 值 ' + entry.price + ' 🪙' : '')))
59
+ chip.appendChild(grow)
60
+ chips.appendChild(chip)
61
+ })(souvenirs[s])
62
+ }
63
+ ui.content.appendChild(chips)
64
+
65
+ // The story card: tapping a souvenir is how the pig tells you where it
66
+ // went and what it brought back.
67
+ var picked = null
68
+ for (var q = 0; q < souvenirs.length; q += 1) if (souvenirs[q].key === ui.souvenirPick) picked = souvenirs[q]
69
+ if (picked !== null) {
70
+ // Named uniquely: the CSS-guard test maps `var x = el(...)` names to
71
+ // classes, and reusing `card` here shadowed the real .dp-card entry.
72
+ var souvenirCard = el('div', 'dp-locked')
73
+ souvenirCard.appendChild(el('div', null, picked.emoji + ' ' + picked.label + ' · ' + picked.rarityEmoji + picked.rarityLabel
74
+ + (picked.fromLabel === '' ? '' : ' · 来自' + picked.fromLabel)))
75
+ souvenirCard.appendChild(el('div', null, picked.story === ''
76
+ ? '(这只纪念品是旧版本带回来的,没有留下故事。)'
77
+ : '「' + picked.story + '」'))
78
+ if (picked.price > 0) {
79
+ var sell = button('dp-mini', { 'data-sell': picked.key }, function () {
80
+ ui.souvenirPick = null
81
+ ui.send('sell', { souvenir: picked.key })
82
+ })
83
+ sell.textContent = '卖掉 +' + picked.price + ' 🪙'
84
+ sell.style.marginTop = '6px'
85
+ souvenirCard.appendChild(sell)
86
+ }
87
+ ui.content.appendChild(souvenirCard)
88
+ }
89
+ }
@@ -0,0 +1,164 @@
1
+ // @ts-check
2
+ /**
3
+ * 更新 App(只在桌面版出现,见 apps/desktop):现在是哪个版本,GitHub 上有哪些版本,
4
+ * 一键更新到最新,或者挑一个版本换过去;换了还能回到上一个。
5
+ *
6
+ * 版本列表一格一个方块,点一个下面出详情卡(更新说明、能不能换、按钮)。
7
+ * 读写都经过桌面版的 window.piggyShell.updates,不走游戏的路由。
8
+ * @module dsh-piggy/client/tabs/update
9
+ */
10
+
11
+ import { button, el } from '../dom.js'
12
+ import { tile, tileGrid } from '../widgets.js'
13
+
14
+ /** One panel per page, so the update state can live here. */
15
+ /** @type {any} */
16
+ var state = {
17
+ current: null, list: null, error: null, loading: false,
18
+ pick: null, busy: null, fraction: 0, message: null, listening: false,
19
+ }
20
+
21
+ /** The desktop bridge, or null inside DSH. */
22
+ export function updatesBridge() {
23
+ var shell = /** @type {any} */ (window).piggyShell
24
+ return shell && shell.updates ? shell : null
25
+ }
26
+
27
+ function refresh(ui) {
28
+ var shell = updatesBridge()
29
+ if (shell === null || state.loading) return
30
+ state.loading = true
31
+ state.error = null
32
+ if (!state.listening) {
33
+ state.listening = true
34
+ shell.updates.onProgress(function (fraction) { state.fraction = fraction; ui.renderContent() })
35
+ }
36
+ Promise.all([shell.updates.current(), shell.updates.list()]).then(function (got) {
37
+ state.current = got[0]
38
+ if (got[1] && got[1].ok) state.list = got[1].releases
39
+ else state.error = (got[1] && got[1].reason) || '没问到'
40
+ }, function () { state.error = '没问到' }).then(function () {
41
+ state.loading = false
42
+ ui.renderContent()
43
+ })
44
+ }
45
+
46
+ function install(ui, version) {
47
+ var shell = updatesBridge()
48
+ if (shell === null) return
49
+ state.busy = version
50
+ state.fraction = 0
51
+ state.message = null
52
+ ui.renderContent()
53
+ shell.updates.install(version).then(function (result) {
54
+ state.message = result.ok ? '换好了,猪马上回来…' : result.reason
55
+ if (!result.ok) state.busy = null
56
+ ui.renderContent()
57
+ })
58
+ }
59
+
60
+ function rollback(ui) {
61
+ var shell = updatesBridge()
62
+ if (shell === null) return
63
+ state.busy = 'rollback'
64
+ ui.renderContent()
65
+ shell.updates.rollback().then(function (result) {
66
+ state.message = result.ok ? '回去了,猪马上回来…' : result.reason
67
+ if (!result.ok) state.busy = null
68
+ ui.renderContent()
69
+ })
70
+ }
71
+
72
+ export function renderUpdateTab(ui) {
73
+ if (updatesBridge() === null) {
74
+ ui.content.appendChild(el('div', 'dp-empty', '桌面版才有这个'))
75
+ return
76
+ }
77
+ if (state.current === null && state.list === null && state.error === null) refresh(ui)
78
+ var cur = state.current
79
+ var head = el('div', 'dp-pick dp-tile-card dp-update-now')
80
+ head.appendChild(el('div', 'dp-pick-head', cur === null ? '正在看现在的版本…'
81
+ : '现在 v' + cur.version + (cur.bundled ? '(安装包自带)' : '')))
82
+ if (cur !== null) head.appendChild(el('div', 'dp-dim', '安装包 ' + cur.shell))
83
+ if (state.message !== null) head.appendChild(el('div', 'dp-req', state.message))
84
+ if (state.busy !== null && state.message === null) {
85
+ head.appendChild(el('div', 'dp-dim', state.busy === 'rollback' ? '正在换回去…' : '下载中 ' + Math.round(state.fraction * 100) + '%'))
86
+ }
87
+ var latest = state.list === null ? null : state.list.find(function (r) { return r.blocked === null && !r.prerelease }) || null
88
+ if (latest !== null && cur !== null && !latest.current) {
89
+ var up = button('dp-btn dp-btn-wide', { 'data-update-latest': latest.version }, function () { install(ui, latest.version) })
90
+ up.textContent = '⬆️ 更新到最新 v' + latest.version
91
+ up.disabled = state.busy !== null
92
+ head.appendChild(up)
93
+ } else if (latest !== null) {
94
+ head.appendChild(el('div', 'dp-req dp-req-ok', '✓ 已经是最新'))
95
+ }
96
+ ui.content.appendChild(head)
97
+
98
+ if (state.loading && state.list === null) ui.content.appendChild(el('div', 'dp-empty', '正在问 GitHub…'))
99
+ if (state.error !== null) {
100
+ ui.content.appendChild(el('div', 'dp-empty', state.error))
101
+ var again = button('dp-btn dp-btn-wide', { 'data-update-retry': '' }, function () { refresh(ui) })
102
+ again.textContent = '再试一次'
103
+ ui.content.appendChild(again)
104
+ }
105
+ if (state.list !== null) renderList(ui, state.list)
106
+
107
+ if (cur !== null && cur.previous !== null) {
108
+ var back = button('dp-btn dp-btn-wide dp-update-back', { 'data-update-rollback': '' }, function () { rollback(ui) })
109
+ back.textContent = '↩️ 回到上一个版本 v' + cur.previous
110
+ back.disabled = state.busy !== null
111
+ ui.content.appendChild(back)
112
+ }
113
+ }
114
+
115
+ /** Every version as a tile; the picked one opens its notes and button below. */
116
+ function renderList(ui, list) {
117
+ if (list.length === 0) {
118
+ ui.content.appendChild(el('div', 'dp-empty', 'GitHub 上还没有能热更新的版本'))
119
+ return
120
+ }
121
+ var grid = tileGrid()
122
+ var picked = null
123
+ for (var i = 0; i < list.length; i += 1) {
124
+ (function (release, first) {
125
+ var active = state.pick === release.version
126
+ if (active) picked = release
127
+ grid.appendChild(tile({
128
+ emoji: release.current ? '🐖' : '📦', label: 'v' + release.version, color: 'lime', soft: true, active: active,
129
+ note: release.date, badge: first && release.blocked === null ? '新' : '',
130
+ tag: release.current ? '在用' : release.blocked !== null ? '🔒' : '', dim: release.blocked !== null,
131
+ data: { 'data-release': release.version },
132
+ onPick: function () { state.pick = active ? null : release.version; ui.renderContent() },
133
+ }))
134
+ })(list[i], i === 0)
135
+ }
136
+ ui.content.appendChild(grid)
137
+ if (picked !== null) ui.content.appendChild(details(ui, picked))
138
+ }
139
+
140
+ function details(ui, release) {
141
+ var box = el('div', 'dp-pick dp-tile-card dp-update-detail')
142
+ box.appendChild(el('div', 'dp-pick-head', 'v' + release.version + (release.date ? ' · ' + release.date : '') + (release.prerelease ? ' · 预览版' : '')))
143
+ if (release.notes) box.appendChild(el('div', 'dp-update-notes', release.notes))
144
+ var go = button('dp-btn dp-btn-wide', { 'data-update-install': release.version }, function () {
145
+ if (release.blocked === 'shell') updatesBridge().openPage(release.page)
146
+ else install(ui, release.version)
147
+ })
148
+ if (release.current) {
149
+ go.textContent = '正在用这个'
150
+ go.disabled = true
151
+ } else if (release.blocked === 'shell') {
152
+ box.appendChild(el('div', 'dp-req', '✗ 要先装 ' + release.minShell + ' 以上的安装包'))
153
+ go.textContent = '去下载新安装包'
154
+ } else if (release.blocked === 'save') {
155
+ box.appendChild(el('div', 'dp-req', '✗ 存档太新,这个版本读不了'))
156
+ go.textContent = '换不了'
157
+ go.disabled = true
158
+ } else {
159
+ go.textContent = '换到这个版本'
160
+ go.disabled = state.busy !== null
161
+ }
162
+ box.appendChild(go)
163
+ return box
164
+ }
@@ -0,0 +1,106 @@
1
+ // @ts-check
2
+ /**
3
+ * 打工页签(B9:和学习 / 商店 / 背包一样的方块)。
4
+ *
5
+ * 第一层:武力 / 魅力 / 智力 三个大方块,右上角写能干的份数。
6
+ * 第二层:这类职业的方块;点一个,下面出详情卡 —— 门槛逐条打勾打叉、加成和消耗、「出发」。
7
+ * @module dsh-piggy/client/tabs/work
8
+ */
9
+
10
+ import { button, el } from '../dom.js'
11
+ import { drillHeader, drillTo, tile, tileGrid } from '../widgets.js'
12
+
13
+ /** The three skills, in the order QQ Pet lists its traits, each with its colour. */
14
+ var SKILLS = [
15
+ { key: 'strong', emoji: '💪', label: '武力', color: 'orange' },
16
+ { key: 'charm', emoji: '✨', label: '魅力', color: 'pink' },
17
+ { key: 'intel', emoji: '🧠', label: '智力', color: 'blue' },
18
+ ]
19
+
20
+ export function renderWorkTab(ui) {
21
+ if (ui.view.jobs.length === 0) {
22
+ ui.content.appendChild(el('div', 'dp-empty', '宿主还没提供工作列表。'))
23
+ return
24
+ }
25
+ // Older hosts send no trait per job: one list of every job, no skill layer.
26
+ var bySkill = ui.view.jobs.some(function (job) { return job.trait !== '' })
27
+ if (!bySkill) {
28
+ renderJobs(ui, ui.view.jobs, 'orange')
29
+ return
30
+ }
31
+ var skill = SKILLS.find(function (entry) { return entry.key === ui.drill.work })
32
+ if (skill === undefined) {
33
+ renderSkills(ui)
34
+ return
35
+ }
36
+ var chosen = skill
37
+ drillHeader(ui, 'work', chosen.emoji + ' ' + chosen.label, '')
38
+ renderJobs(ui, ui.view.jobs.filter(function (job) { return job.trait === chosen.key }), chosen.color)
39
+ }
40
+
41
+ /** The top layer: one tile per skill, with how many of its jobs the pig can do. */
42
+ function renderSkills(ui) {
43
+ var grid = tileGrid()
44
+ for (var s = 0; s < SKILLS.length; s += 1) {
45
+ (function (skill) {
46
+ var open = ui.view.jobs.filter(function (job) { return job.trait === skill.key && job.qualified }).length
47
+ grid.appendChild(tile({
48
+ emoji: skill.emoji, label: skill.label, color: skill.color,
49
+ badge: open > 0 ? String(open) : '',
50
+ data: { 'data-skill': skill.key },
51
+ onPick: function () { drillTo(ui, 'work', skill.key) },
52
+ }))
53
+ })(SKILLS[s])
54
+ }
55
+ ui.content.appendChild(grid)
56
+ }
57
+
58
+ /** Job tiles; the picked one opens its details under the grid. */
59
+ function renderJobs(ui, jobs, color) {
60
+ var grid = tileGrid()
61
+ var picked = null
62
+ for (var i = 0; i < jobs.length; i += 1) {
63
+ (function (job) {
64
+ var active = ui.drill.pick === job.key
65
+ if (active) picked = job
66
+ var locked = job.qualified === false
67
+ grid.appendChild(tile({
68
+ emoji: job.emoji, label: job.label, color: color, soft: true, active: active,
69
+ note: job.minutes + '分·' + job.coins + '🪙',
70
+ tag: locked ? '🔒' : '', dim: locked,
71
+ data: { 'data-job-tile': job.key },
72
+ onPick: function () {
73
+ ui.drill.pick = active ? null : job.key
74
+ ui.renderContent()
75
+ },
76
+ }))
77
+ })(jobs[i])
78
+ }
79
+ ui.content.appendChild(grid)
80
+ if (picked !== null) ui.content.appendChild(jobDetails(ui, picked))
81
+ }
82
+
83
+ /** 详情: every condition with a tick or a cross, what the job pays and costs, and 出发. */
84
+ function jobDetails(ui, job) {
85
+ var box = el('div', 'dp-pick dp-tile-card dp-job-detail')
86
+ box.appendChild(el('div', 'dp-pick-head', job.emoji + ' ' + job.label + ' · ' + (job.qualified ? '条件都够了' : '还差这些')))
87
+ for (var r = 0; r < job.requirements.length; r += 1) {
88
+ var need = job.requirements[r]
89
+ var have = need.kind === 'level' ? '(现在 Lv.' + need.have + ')'
90
+ : need.kind === 'certificate' ? '(' + need.have + '/' + need.need + ' 次)'
91
+ : need.kind === 'every' || need.kind === 'anyOf' ? '(' + need.have + '/' + need.need + ' 门)'
92
+ : '(现在 ' + need.have + ' 节)'
93
+ box.appendChild(el('div', need.met ? 'dp-req dp-req-ok' : 'dp-req', (need.met ? '✓ ' : '✗ ') + need.text + (need.met ? '' : ' ' + have)))
94
+ }
95
+ // A locked job from an older host has no checklist, only the summary line.
96
+ if (job.requirements.length === 0 && job.lockText) box.appendChild(el('div', 'dp-req', '✗ ' + job.lockText))
97
+ box.appendChild(el('div', 'dp-dim', job.minutes + ' 分钟 · ' + job.coins + ' 🪙 · '
98
+ + job.traitEmoji + job.traitLabel + ' ' + job.traitPoints
99
+ + (job.payPercent > 0 ? '(+' + job.payPercent + '%)' : '')
100
+ + ' · 饱食 ' + job.satiety + ' · 清洁 ' + job.cleanliness))
101
+ var go = button('dp-btn dp-btn-wide dp-job-go', { 'data-job': job.key }, function () { ui.send('work', { job: job.key }) })
102
+ go.textContent = '💼 出发'
103
+ go.disabled = !ui.view.canGoOut || job.qualified === false
104
+ box.appendChild(go)
105
+ return box
106
+ }
@@ -0,0 +1,11 @@
1
+ // @ts-check
2
+ /**
3
+ * 防御性读取:把任何宿主载荷映射成确定的形状。
4
+ *
5
+ * @module dsh-piggy/client/values
6
+ */
7
+ export var isObj = v => typeof v === 'object' && v !== null && !Array.isArray(v)
8
+ export var obj = v => (isObj(v) ? v : {})
9
+ export var arr = v => (Array.isArray(v) ? v : [])
10
+ export var num = (v, dflt) => (typeof v === 'number' && isFinite(v) ? v : dflt)
11
+ export var str = (v, dflt) => (typeof v === 'string' && v !== '' ? v : dflt)
@@ -0,0 +1,126 @@
1
+ // @ts-check
2
+ /**
3
+ * 页签共用的小组件:属性条、道具选择器、效果行。
4
+ *
5
+ * 只负责画,不决定业务规则。
6
+ * @module dsh-piggy/client/widgets
7
+ */
8
+
9
+ import { button, el, meter } from './dom.js'
10
+ import { num } from './values.js'
11
+
12
+ export function labelledBar(ui, label, value, valueText, variant) {
13
+ var row = el('div', 'dp-row')
14
+ row.appendChild(el('span', null, label))
15
+ row.appendChild(el('b', null, valueText))
16
+ ui.content.appendChild(row)
17
+ ui.content.appendChild(meter(value, variant))
18
+ }
19
+
20
+ export function pickerPanel(ui, action) {
21
+ var wrap = el('div', 'dp-pick')
22
+ var asks = { feed: '喂点什么?', bathe: '用哪个洗澡?', play: '拿哪个玩具?' }
23
+ wrap.appendChild(el('div', 'dp-pick-head', asks[action] ?? '用哪个?'))
24
+ var list = el('div', 'dp-list')
25
+ var shelf = ui.view.care[action] ?? []
26
+ for (var i = 0; i < shelf.length; i += 1) {
27
+ (function (item) {
28
+ var row = el('div', 'dp-item')
29
+ row.appendChild(el('span', null, item.emoji))
30
+ var grow = el('div', 'dp-grow')
31
+ grow.appendChild(el('div', null, item.label + (item.default ? '(自带)' : ' ×' + num(item.count, 0))))
32
+ grow.appendChild(el('div', 'dp-dim', careEffectLine(action, item)))
33
+ row.appendChild(grow)
34
+ var use = button('dp-mini', { 'data-care': action + ':' + item.key }, function () {
35
+ ui.picker = null
36
+ ui.send(action, { item: item.key })
37
+ })
38
+ use.textContent = '用'
39
+ row.appendChild(use)
40
+ list.appendChild(row)
41
+ })(shelf[i])
42
+ }
43
+ wrap.appendChild(list)
44
+ var cancel = button('dp-cancel', {}, function () { ui.picker = null; ui.renderContent() })
45
+ cancel.textContent = '算了'
46
+ wrap.appendChild(cancel)
47
+ return wrap
48
+ }
49
+
50
+ export function careEffectLine(action, item) {
51
+ var parts = []
52
+ if (action === 'feed') {
53
+ parts.push('饱食 +' + item.satiety)
54
+ if (item.happiness) parts.push('心情 +' + item.happiness)
55
+ } else if (action === 'bathe') {
56
+ parts.push('清洁 +' + item.cleanliness)
57
+ if (item.happiness) parts.push('心情 +' + item.happiness)
58
+ } else {
59
+ parts.push('心情 +' + item.happiness)
60
+ if (item.satiety) parts.push('饱食 ' + item.satiety)
61
+ }
62
+ return parts.join(' · ')
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // 方块(动森手机那种):学习 / 商店 / 背包的两层都用它(B8,用户 2026-10-01)。
67
+ // 第一层是彩色大方块;点进去整屏换成第二层,左上角「‹ 返回」。
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /** The grid tiles sit in: always three columns. */
71
+ export function tileGrid() {
72
+ return el('div', 'dp-tiles')
73
+ }
74
+
75
+ /**
76
+ * One tile: a coloured rounded square with a big emoji, one short line under
77
+ * it, an optional second line, a count in the top-right corner and a small
78
+ * tag in the top-left. `soft` is the paler second-layer look.
79
+ * @param {{ emoji: string, label: string, color: string, note?: string, badge?: string, tag?: string,
80
+ * soft?: boolean, locked?: boolean, dim?: boolean, disabled?: boolean, active?: boolean,
81
+ * data?: Record<string, string>, onPick: () => void }} spec
82
+ */
83
+ export function tile(spec) {
84
+ var node = button('dp-tile' + (spec.soft ? ' dp-tile-soft' : ''), spec.data ?? {}, function () { spec.onPick() })
85
+ node.setAttribute('data-color', spec.color)
86
+ if (spec.locked) node.setAttribute('data-locked', 'true')
87
+ if (spec.dim) node.setAttribute('data-dim', 'true')
88
+ if (spec.active) node.setAttribute('data-active', 'true')
89
+ if (spec.disabled === true) node.disabled = true
90
+ var icon = el('span', 'dp-tile-icon')
91
+ icon.appendChild(el('span', 'dp-tile-e', spec.emoji))
92
+ if (spec.badge) icon.appendChild(el('b', 'dp-tile-badge', spec.badge))
93
+ if (spec.tag) icon.appendChild(el('b', 'dp-tile-tag', spec.tag))
94
+ node.appendChild(icon)
95
+ node.appendChild(el('span', 'dp-tile-n', spec.label))
96
+ if (spec.note) node.appendChild(el('span', 'dp-tile-note', spec.note))
97
+ return node
98
+ }
99
+
100
+ /**
101
+ * Open a category (or go back with `key` null). The new layer starts at the
102
+ * top: the old scroll offset belonged to a different screen.
103
+ * @param {object} ui
104
+ * @param {'study'|'shop'|'bag'|'work'} tab
105
+ * @param {string|null} key
106
+ */
107
+ export function drillTo(ui, tab, key) {
108
+ ui.drill[tab] = key
109
+ ui.drill.pick = null
110
+ ui.renderContent()
111
+ ui.content.scrollTop = 0
112
+ }
113
+
114
+ /**
115
+ * The second layer's top row: 「‹」 back, the category's name, and one grey
116
+ * line of what is true for everything in it (so no tile has to repeat it).
117
+ */
118
+ export function drillHeader(ui, tab, title, info) {
119
+ var row = el('div', 'dp-drill')
120
+ var back = button('dp-drill-back', { 'data-back': tab }, function () { drillTo(ui, tab, null) })
121
+ back.textContent = '‹'
122
+ row.appendChild(back)
123
+ row.appendChild(el('b', 'dp-drill-title', title))
124
+ if (info) row.appendChild(el('span', 'dp-drill-info', info))
125
+ ui.content.appendChild(row)
126
+ }