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.
- package/CHANGELOG.md +1348 -0
- package/LICENSE +32 -0
- package/README.md +322 -0
- package/THIRD-PARTY.md +31 -0
- package/assets/elder.svg +35 -0
- package/assets/pig-king-bathe.svg +38 -0
- package/assets/pig-king-eat.svg +45 -0
- package/assets/pig-king-pet.svg +45 -0
- package/assets/pig-king-play.svg +45 -0
- package/assets/pig-king-relaxed.svg +56 -0
- package/assets/pig-king-study.svg +45 -0
- package/assets/pig-king-trip.svg +45 -0
- package/assets/pig-king-work.svg +45 -0
- package/assets/pig-king.svg +56 -0
- package/assets/piglet.svg +30 -0
- package/client.js +3438 -0
- package/commands.js +252 -0
- package/cordis.patch.yml +15 -0
- package/core.js +11 -0
- package/data.js +10 -0
- package/index.js +68 -0
- package/package.json +72 -0
- package/packages/pet-core/package.json +21 -0
- package/packages/pet-core/src/core/activity.js +80 -0
- package/packages/pet-core/src/core/care.js +160 -0
- package/packages/pet-core/src/core/clock.js +122 -0
- package/packages/pet-core/src/core/constants.js +109 -0
- package/packages/pet-core/src/core/daily.js +204 -0
- package/packages/pet-core/src/core/diary.js +108 -0
- package/packages/pet-core/src/core/drops.js +82 -0
- package/packages/pet-core/src/core/effects.js +84 -0
- package/packages/pet-core/src/core/egg.js +120 -0
- package/packages/pet-core/src/core/evolution.js +90 -0
- package/packages/pet-core/src/core/growth.js +110 -0
- package/packages/pet-core/src/core/illness.js +235 -0
- package/packages/pet-core/src/core/interests.js +55 -0
- package/packages/pet-core/src/core/inventory.js +177 -0
- package/packages/pet-core/src/core/lines.js +157 -0
- package/packages/pet-core/src/core/migrate.js +226 -0
- package/packages/pet-core/src/core/profile.js +102 -0
- package/packages/pet-core/src/core/random.js +79 -0
- package/packages/pet-core/src/core/school.js +81 -0
- package/packages/pet-core/src/core/settlement.js +308 -0
- package/packages/pet-core/src/core/state.js +210 -0
- package/packages/pet-core/src/core/travel.js +58 -0
- package/packages/pet-core/src/core/upgrades.js +193 -0
- package/packages/pet-core/src/core/views.js +55 -0
- package/packages/pet-core/src/core/work.js +50 -0
- package/packages/pet-core/src/core.js +95 -0
- package/packages/pet-core/src/data/daily.js +121 -0
- package/packages/pet-core/src/data/drops.js +34 -0
- package/packages/pet-core/src/data/evolution.js +41 -0
- package/packages/pet-core/src/data/growth.js +72 -0
- package/packages/pet-core/src/data/illness.js +188 -0
- package/packages/pet-core/src/data/interests.js +39 -0
- package/packages/pet-core/src/data/jobs.js +150 -0
- package/packages/pet-core/src/data/life.js +88 -0
- package/packages/pet-core/src/data/lines.js +198 -0
- package/packages/pet-core/src/data/minutes.js +23 -0
- package/packages/pet-core/src/data/profile.js +60 -0
- package/packages/pet-core/src/data/school.js +99 -0
- package/packages/pet-core/src/data/shop.js +142 -0
- package/packages/pet-core/src/data/traits.js +42 -0
- package/packages/pet-core/src/data/travel.js +87 -0
- package/packages/pet-core/src/data.js +52 -0
- package/packages/pet-core/src/index.js +12 -0
- package/render.js +357 -0
- package/routes.js +201 -0
- package/scripts/build-client.mjs +100 -0
- package/scripts/typecheck.mjs +122 -0
- package/snapshot.js +368 -0
- package/src/client/art.js +29 -0
- package/src/client/constants.js +88 -0
- package/src/client/css-base.js +225 -0
- package/src/client/css-card.js +63 -0
- package/src/client/css-tabs.js +250 -0
- package/src/client/css-tiles.js +89 -0
- package/src/client/dom.js +33 -0
- package/src/client/effects.js +159 -0
- package/src/client/format.js +27 -0
- package/src/client/index.js +393 -0
- package/src/client/io.js +100 -0
- package/src/client/layout.js +103 -0
- package/src/client/normalize.js +352 -0
- package/src/client/panel.js +319 -0
- package/src/client/scene.js +113 -0
- package/src/client/storage.js +19 -0
- package/src/client/styles.js +13 -0
- package/src/client/tabs/bag.js +203 -0
- package/src/client/tabs/card.js +110 -0
- package/src/client/tabs/crown.js +59 -0
- package/src/client/tabs/dev.js +101 -0
- package/src/client/tabs/home.js +72 -0
- package/src/client/tabs/shop.js +78 -0
- package/src/client/tabs/status.js +223 -0
- package/src/client/tabs/study.js +128 -0
- package/src/client/tabs/travel.js +89 -0
- package/src/client/tabs/update.js +164 -0
- package/src/client/tabs/work.js +106 -0
- package/src/client/values.js +11 -0
- package/src/client/widgets.js +126 -0
- package/store/api.js +260 -0
- package/store/state-file.js +129 -0
- 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
|
+
}
|