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
package/src/client/io.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 与宿主对话:拉快照、发动作。
|
|
4
|
+
*
|
|
5
|
+
* 只负责 HTTP 与错误话术,画界面交给 panel.js(见 docs/CONVENTIONS.md)。
|
|
6
|
+
* @module dsh-piggy/client/io
|
|
7
|
+
*/
|
|
8
|
+
import { ACT_URL, NO_ITEM_LINE, STATE_URL } from './constants.js'
|
|
9
|
+
import { el } from './dom.js'
|
|
10
|
+
import { num, str } from './values.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {object} ctx - the shell context
|
|
14
|
+
*/
|
|
15
|
+
export function createIo(ctx) {
|
|
16
|
+
/** Bumped by every action, so a stale poll can tell it has been overtaken. */
|
|
17
|
+
var actionSeq = 0
|
|
18
|
+
|
|
19
|
+
async function send(action, extra) {
|
|
20
|
+
if (ctx.busy || ctx.stopped) return
|
|
21
|
+
if (ctx.view.pig === null && action !== 'hatch') return
|
|
22
|
+
// Every action bumps the sequence: a poll that started before this
|
|
23
|
+
// action is stale by the time it lands and must be dropped.
|
|
24
|
+
actionSeq += 1
|
|
25
|
+
ctx.busy = true
|
|
26
|
+
ctx.flash(action)
|
|
27
|
+
try {
|
|
28
|
+
var body = { action: action }
|
|
29
|
+
if (extra) for (var k in extra) body[k] = extra[k]
|
|
30
|
+
var res = await fetch(ACT_URL, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
})
|
|
35
|
+
var next = await res.json()
|
|
36
|
+
ctx.render(next)
|
|
37
|
+
if (next && next.ok === false) {
|
|
38
|
+
// Answering a line that has already moved on is normal (a second
|
|
39
|
+
// window, a slow poll): say nothing rather than scold the user.
|
|
40
|
+
if (next.reason === 'stale-line') return
|
|
41
|
+
// The pig chose not to speak up (away, ill, 免打扰): nothing to say.
|
|
42
|
+
if (next.reason === 'silent') return
|
|
43
|
+
ctx.react('refuse', 520)
|
|
44
|
+
if (next.reason === 'no-item') {
|
|
45
|
+
var emptyKind = str(next.kind, '')
|
|
46
|
+
ctx.showBubble(NO_ITEM_LINE[emptyKind] ?? '背包里没有能用的东西', 3200)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
var reasons = {
|
|
50
|
+
box: '先把纸盒拆开',
|
|
51
|
+
'coronation-ineligible': '加冕条件还没齐',
|
|
52
|
+
cooldown: '还要等 ' + num(next.wait, 0) + ' 秒',
|
|
53
|
+
poor: '钱不够',
|
|
54
|
+
away: '它在外面',
|
|
55
|
+
weak: '太虚弱了,先养好再出门',
|
|
56
|
+
hungry: '太饿了',
|
|
57
|
+
'wrong-medicine': '药不对症,病情加重了…',
|
|
58
|
+
empty: '背包里没有',
|
|
59
|
+
'not-sick': '它没生病',
|
|
60
|
+
dead: '它已经走了…',
|
|
61
|
+
idle: '它没在外面',
|
|
62
|
+
owned: '这件已经有了',
|
|
63
|
+
'low-level': '等级不够(要 Lv.' + num(next.need, 0) + ',现在 Lv.' + num(next.have, 0) + ')',
|
|
64
|
+
'not-owned': '还没有这件东西',
|
|
65
|
+
'not-consumable': '这个是穿的,不是用的',
|
|
66
|
+
'wrong-stage': '这个学段没有这门课',
|
|
67
|
+
underqualified: '它还没这个本事,先去上课',
|
|
68
|
+
}
|
|
69
|
+
ctx.showBubble(reasons[next.reason] ?? '这个操作没成', 2400)
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
ctx.showBubble('操作没送到宿主', 2600)
|
|
73
|
+
ctx.react('refuse', 520)
|
|
74
|
+
} finally {
|
|
75
|
+
ctx.busy = false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function refresh() {
|
|
80
|
+
if (ctx.stopped) return
|
|
81
|
+
// A poll can change the live content (a job finishing, an illness
|
|
82
|
+
// starting), so re-check the panel still fits.
|
|
83
|
+
ctx.fitPanel()
|
|
84
|
+
var startedAt = actionSeq
|
|
85
|
+
try {
|
|
86
|
+
var res = await fetch(STATE_URL, { cache: 'no-store' })
|
|
87
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
88
|
+
var next = await res.json()
|
|
89
|
+
// An action landed while this poll was in flight: its result is newer
|
|
90
|
+
// than ours, so painting ours would undo what the user just did.
|
|
91
|
+
if (startedAt !== actionSeq) return
|
|
92
|
+
ctx.render(next)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (ctx.stopped) return
|
|
95
|
+
ctx.showBubble('连接不上宿主', 4000)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { send: send, refresh: refresh }
|
|
100
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 图标栏与布局:钳制位置、贴边放下、图标的建立与高亮
|
|
4
|
+
*
|
|
5
|
+
* 只通过 ctx 读写外壳的状态与元素(getter/setter 转发),不直接碰全局。
|
|
6
|
+
* @module dsh-piggy/client/layout
|
|
7
|
+
*/
|
|
8
|
+
import { DEV_TAB, PANEL_GAP, PANEL_MARGIN, PANEL_MIN_HEIGHT, PANEL_WIDTH, PIG_PADDING_X, SCENE_RESERVE, TABS } from './constants.js'
|
|
9
|
+
import { button, el } from './dom.js'
|
|
10
|
+
|
|
11
|
+
export function createLayout(ctx) {
|
|
12
|
+
function clampPig() {
|
|
13
|
+
var vw = window.innerWidth || 0
|
|
14
|
+
var vh = window.innerHeight || 0
|
|
15
|
+
if (vw <= 0 || vh <= 0) return
|
|
16
|
+
// Bound by the pig, not by the scene. The scene widens to the panel when
|
|
17
|
+
// open, and clamping against that would shove the pig sideways on any
|
|
18
|
+
// window resize; the panel's own overflow is `fitPanel`'s problem.
|
|
19
|
+
var pigRect = ctx.pig.getBoundingClientRect ? ctx.pig.getBoundingClientRect() : null
|
|
20
|
+
var w = (pigRect ? pigRect.width || 0 : 0) + 2 * PIG_PADDING_X
|
|
21
|
+
// Vertically reserve the OPEN scene, or a pig parked high up pushes its
|
|
22
|
+
// own hud off the top of the window the moment the panel opens.
|
|
23
|
+
var sceneRect = ctx.scene.getBoundingClientRect ? ctx.scene.getBoundingClientRect() : null
|
|
24
|
+
var h = Math.max(sceneRect ? sceneRect.height || 0 : 0, SCENE_RESERVE)
|
|
25
|
+
var right = Math.min(Math.max(4, ctx.userRight), Math.max(4, vw - w - 4))
|
|
26
|
+
// Only the pig anchors vertically. Clamping by the open panel would move
|
|
27
|
+
// the pig when the panel appears, which is the one thing the layout
|
|
28
|
+
// exists to prevent — `fitPanel` shrinks the panel instead.
|
|
29
|
+
var bottom = Math.min(Math.max(4, ctx.userBottom), Math.max(4, vh - h - 4))
|
|
30
|
+
ctx.host.style.right = Math.round(right) + 'px'
|
|
31
|
+
ctx.host.style.bottom = Math.round(bottom) + 'px'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fitPanel() {
|
|
35
|
+
if (!ctx.isOpen) return
|
|
36
|
+
var vw = window.innerWidth || 0
|
|
37
|
+
var vh = window.innerHeight || 0
|
|
38
|
+
if (vw <= 0 || vh <= 0) return
|
|
39
|
+
|
|
40
|
+
var rect = ctx.scene.getBoundingClientRect()
|
|
41
|
+
var roomAbove = rect.top - PANEL_GAP - PANEL_MARGIN
|
|
42
|
+
var roomBelow = vh - rect.bottom - PANEL_GAP - PANEL_MARGIN
|
|
43
|
+
|
|
44
|
+
// Open on whichever side has space. Ties go above, which is where a
|
|
45
|
+
// bottom-docked pig expects its menu — but a pig parked near the top
|
|
46
|
+
// must flip, otherwise its own menu opens off the screen.
|
|
47
|
+
// Exactly one of top/bottom may apply. Clearing with '' would fall back
|
|
48
|
+
// to the stylesheet's `bottom`, leaving both set — and an absolutely
|
|
49
|
+
// positioned box with both edges pinned collapses to zero height.
|
|
50
|
+
if (roomAbove >= roomBelow) {
|
|
51
|
+
ctx.card.style.top = 'auto'
|
|
52
|
+
ctx.card.style.bottom = 'calc(100% + ' + PANEL_GAP + 'px)'
|
|
53
|
+
ctx.card.style.maxHeight = Math.max(PANEL_MIN_HEIGHT, Math.round(roomAbove)) + 'px'
|
|
54
|
+
} else {
|
|
55
|
+
ctx.card.style.bottom = 'auto'
|
|
56
|
+
ctx.card.style.top = 'calc(100% + ' + PANEL_GAP + 'px)'
|
|
57
|
+
ctx.card.style.maxHeight = Math.max(PANEL_MIN_HEIGHT, Math.round(roomBelow)) + 'px'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Horizontal: the panel is wider than the pig, so anchoring its right
|
|
61
|
+
// edge to the pig can push it off the left of the window. A negative
|
|
62
|
+
// `right` moves the panel without touching the wrapper's width, so the
|
|
63
|
+
// pig stays exactly where it was put.
|
|
64
|
+
var width = Math.min(PANEL_WIDTH, vw - 2 * PANEL_MARGIN)
|
|
65
|
+
ctx.card.style.maxWidth = Math.round(width) + 'px'
|
|
66
|
+
var shift = PANEL_MARGIN - (rect.right - width)
|
|
67
|
+
ctx.card.style.right = shift > 0 ? -Math.round(shift) + 'px' : '0px'
|
|
68
|
+
|
|
69
|
+
// The hud lives inside the scene, which runs past the left edge whenever
|
|
70
|
+
// the panel above had to be shifted back into view. Line it up with the
|
|
71
|
+
// panel's left edge so it stays visible too.
|
|
72
|
+
var cardLeft = Math.max(rect.right - width, PANEL_MARGIN)
|
|
73
|
+
ctx.hud.style.left = Math.max(9, Math.round(cardLeft - rect.left)) + 'px'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function visibleTabs() {
|
|
77
|
+
return ctx.devMode ? TABS.concat([DEV_TAB]) : TABS
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function paintBar() {
|
|
81
|
+
while (ctx.bar.firstChild) ctx.bar.removeChild(ctx.bar.firstChild)
|
|
82
|
+
var list = visibleTabs()
|
|
83
|
+
for (var t = 0; t < list.length; t += 1) ctx.buildIcon(list[t])
|
|
84
|
+
// The home screen has no icon of its own; only a vanished app (dev off) falls back.
|
|
85
|
+
if (ctx.icons[ctx.tab] === undefined && ctx.tab !== 'home') ctx.tab = 'home'
|
|
86
|
+
for (var k in ctx.icons) ctx.icons[k].setAttribute('data-active', k === ctx.tab ? 'true' : 'false')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildIcon(entry) {
|
|
90
|
+
(function (entry) {
|
|
91
|
+
var btn = button('dp-ico', { 'data-tab': entry.key }, function () {
|
|
92
|
+
if (ctx.host.getAttribute('data-open') !== 'true') ctx.setOpen(true)
|
|
93
|
+
ctx.select(entry.key)
|
|
94
|
+
})
|
|
95
|
+
btn.appendChild(el('span', 'dp-ico-e', entry.emoji))
|
|
96
|
+
btn.appendChild(el('span', null, entry.label))
|
|
97
|
+
ctx.icons[entry.key] = btn
|
|
98
|
+
ctx.bar.appendChild(btn)
|
|
99
|
+
})(entry)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { clampPig, fitPanel, visibleTabs, paintBar, buildIcon }
|
|
103
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 把宿主快照(当前版 / 旧版 / 被截断)映射成面板画的那份形状。
|
|
4
|
+
*
|
|
5
|
+
* 缺字段补默认值,永不为 `undefined` —— 这是"满屏 undefined"那个 bug 的修法。
|
|
6
|
+
* @module dsh-piggy/client/normalize
|
|
7
|
+
*/
|
|
8
|
+
import { MODES } from './constants.js'
|
|
9
|
+
import { arr, isObj, num, obj, str } from './values.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map any host payload — current, older, or truncated — onto the exact
|
|
13
|
+
* shape the panel draws. Missing fields become defaults, never `undefined`.
|
|
14
|
+
* @returns {object} a fully populated view model.
|
|
15
|
+
*/
|
|
16
|
+
export function normalize(raw) {
|
|
17
|
+
var d = obj(raw)
|
|
18
|
+
var pig = isObj(d.pig) ? d.pig : null
|
|
19
|
+
var legacy = pig !== null && !('coins' in pig) && !('health' in pig)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
legacy: legacy,
|
|
23
|
+
// Host build version, shown in the debug tab so a stale bundle is
|
|
24
|
+
// visible instead of being guessed at.
|
|
25
|
+
version: str(d.version, ''),
|
|
26
|
+
// Trust the flag when the host sends one. Older hosts did not, and for
|
|
27
|
+
// those "a pig exists" is still the right answer.
|
|
28
|
+
hatched: d.hatched === true || (d.hatched === undefined && pig !== null),
|
|
29
|
+
dead: d.dead === true || (pig !== null && num(pig.health, 5) <= 0),
|
|
30
|
+
pig: pig === null ? null : {
|
|
31
|
+
name: str(pig.name, '猪猪'),
|
|
32
|
+
// The pig is measured in days now; `stage` carries how big it is and
|
|
33
|
+
// what it looks like.
|
|
34
|
+
stage: {
|
|
35
|
+
key: str(obj(pig.stage).key, 'piglet'),
|
|
36
|
+
label: str(obj(pig.stage).label, '小猪'),
|
|
37
|
+
emoji: str(obj(pig.stage).emoji, '🐖'),
|
|
38
|
+
size: num(obj(pig.stage).size, 56),
|
|
39
|
+
line: str(obj(pig.stage).line, ''),
|
|
40
|
+
art: typeof obj(pig.stage).art === 'string' && obj(pig.stage).art !== '' ? obj(pig.stage).art : null,
|
|
41
|
+
faded: obj(pig.stage).faded === true,
|
|
42
|
+
// 加冕后的形态:有没有动作立绘、盖住哪些装扮位置。
|
|
43
|
+
actionArt: obj(pig.stage).actionArt === true,
|
|
44
|
+
hides: arr(obj(pig.stage).hides).map(function (slot) { return str(slot, '') }),
|
|
45
|
+
},
|
|
46
|
+
// Older hosts send no sex; the HUD then simply shows none.
|
|
47
|
+
sex: isObj(pig.sex) ? { key: str(pig.sex.key, ''), label: str(pig.sex.label, ''), symbol: str(pig.sex.symbol, '') } : null,
|
|
48
|
+
ageLabel: str(pig.ageLabel, ''),
|
|
49
|
+
ageForced: pig.ageForced === true,
|
|
50
|
+
daysToNextStage: typeof pig.daysToNextStage === 'number' ? pig.daysToNextStage : null,
|
|
51
|
+
soul: pig.soul === true,
|
|
52
|
+
mood: str(pig.mood, 'fine'),
|
|
53
|
+
moodEmoji: str(pig.moodEmoji, '😊'),
|
|
54
|
+
moodLabel: str(pig.moodLabel, '还不错'),
|
|
55
|
+
satiety: Math.round(num(pig.satiety, 0)),
|
|
56
|
+
happiness: Math.round(num(pig.happiness, 0)),
|
|
57
|
+
cleanliness: Math.round(num(pig.cleanliness, 0)),
|
|
58
|
+
health: num(pig.health, 5),
|
|
59
|
+
healthPercent: num(pig.healthPercent, 100),
|
|
60
|
+
coins: num(pig.coins, 0),
|
|
61
|
+
weight: str(pig.weight, '—'),
|
|
62
|
+
xp: num(pig.xp, 0),
|
|
63
|
+
// Level is driven by growth and decides the body (B2).
|
|
64
|
+
level: (function (info) {
|
|
65
|
+
var i = obj(info)
|
|
66
|
+
var t = obj(i.title)
|
|
67
|
+
return {
|
|
68
|
+
level: num(i.level, 1),
|
|
69
|
+
percent: num(i.percent, 0),
|
|
70
|
+
toNext: num(i.toNext, 0),
|
|
71
|
+
maxed: i.maxed === true,
|
|
72
|
+
titleLabel: str(t.label, '新来的'),
|
|
73
|
+
titleEmoji: str(t.emoji, '🌱'),
|
|
74
|
+
}
|
|
75
|
+
})(pig.levelInfo),
|
|
76
|
+
stageLine: str(pig.stageLine, ''),
|
|
77
|
+
illness: isObj(pig.illness) ? {
|
|
78
|
+
name: str(pig.illness.name, '生病'),
|
|
79
|
+
cure: str(pig.illness.cure, '药'),
|
|
80
|
+
cureEmoji: str(pig.illness.cureEmoji, '💊'),
|
|
81
|
+
stage: num(pig.illness.stage, 1),
|
|
82
|
+
doctorFee: typeof pig.illness.doctorFee === 'number' ? pig.illness.doctorFee : null,
|
|
83
|
+
} : null,
|
|
84
|
+
traits: {
|
|
85
|
+
intel: num(obj(pig.traits).intel, 0),
|
|
86
|
+
charm: num(obj(pig.traits).charm, 0),
|
|
87
|
+
strong: num(obj(pig.traits).strong, 0),
|
|
88
|
+
},
|
|
89
|
+
courses: obj(pig.courses),
|
|
90
|
+
// Souvenirs are objects now (rarity + story). An old host sent bare
|
|
91
|
+
// strings, and those must still list rather than turn into [object
|
|
92
|
+
// Object] or vanish.
|
|
93
|
+
souvenirs: arr(pig.souvenirs).map(entry => {
|
|
94
|
+
if (typeof entry === 'string') {
|
|
95
|
+
return { key: entry, emoji: '🎁', label: entry, rarityLabel: '普通', rarityEmoji: '⚪', price: 0, story: '', fromLabel: '' }
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
key: str(obj(entry).key, ''),
|
|
99
|
+
emoji: str(obj(entry).emoji, '🎁'),
|
|
100
|
+
label: str(obj(entry).label, '纪念品'),
|
|
101
|
+
rarityLabel: str(obj(entry).rarityLabel, '普通'),
|
|
102
|
+
rarityEmoji: str(obj(entry).rarityEmoji, '⚪'),
|
|
103
|
+
price: num(obj(entry).price, 0),
|
|
104
|
+
story: str(obj(entry).story, ''),
|
|
105
|
+
fromLabel: str(obj(entry).fromLabel, ''),
|
|
106
|
+
}
|
|
107
|
+
}).filter(entry => entry.key !== ''),
|
|
108
|
+
memories: arr(pig.memories).filter(m => typeof m === 'string'),
|
|
109
|
+
},
|
|
110
|
+
actions: normalizeActions(d.actions),
|
|
111
|
+
jobs: arr(d.jobs).map(job => ({
|
|
112
|
+
key: str(obj(job).key, ''),
|
|
113
|
+
label: str(obj(job).label, '工作'),
|
|
114
|
+
emoji: str(obj(job).emoji, '💼'),
|
|
115
|
+
minutes: num(obj(job).minutes, 0),
|
|
116
|
+
coins: num(obj(job).coins, 0),
|
|
117
|
+
available: obj(job).available === true,
|
|
118
|
+
// What schooling has bought this job.
|
|
119
|
+
traitLabel: str(obj(job).traitLabel, ''),
|
|
120
|
+
traitEmoji: str(obj(job).traitEmoji, ''),
|
|
121
|
+
traitPoints: num(obj(job).traitPoints, 0),
|
|
122
|
+
baseMinutes: num(obj(job).baseMinutes, 0),
|
|
123
|
+
baseCoins: num(obj(job).baseCoins, 0),
|
|
124
|
+
payPercent: num(obj(job).payPercent, 0),
|
|
125
|
+
speedPercent: num(obj(job).speedPercent, 0),
|
|
126
|
+
// An old host has no gate at all, so a missing flag must read as
|
|
127
|
+
// "qualified" — the opposite default would lock every job on upgrade.
|
|
128
|
+
qualified: obj(job).qualified !== false,
|
|
129
|
+
lockText: str(obj(job).lockText, ''),
|
|
130
|
+
level: num(obj(job).level, 1),
|
|
131
|
+
trait: str(obj(job).trait, ''),
|
|
132
|
+
satiety: num(obj(job).satiety, 0),
|
|
133
|
+
cleanliness: num(obj(job).cleanliness, 0),
|
|
134
|
+
requirements: arr(obj(job).requirements).filter(isObj).map(entry => ({
|
|
135
|
+
text: str(entry.text, ''),
|
|
136
|
+
need: num(entry.need, 0),
|
|
137
|
+
have: num(entry.have, 0),
|
|
138
|
+
kind: str(entry.kind, ''),
|
|
139
|
+
met: entry.met === true,
|
|
140
|
+
})),
|
|
141
|
+
})).filter(job => job.key !== ''),
|
|
142
|
+
// B4: nine subjects, each with its own lesson count and stage.
|
|
143
|
+
subjects: arr(d.subjects).map(sub => ({
|
|
144
|
+
key: str(obj(sub).key, ''),
|
|
145
|
+
label: str(obj(sub).label, '课'),
|
|
146
|
+
emoji: str(obj(sub).emoji, '📘'),
|
|
147
|
+
traitLabel: str(obj(sub).traitLabel, ''),
|
|
148
|
+
traitEmoji: str(obj(sub).traitEmoji, ''),
|
|
149
|
+
lessons: num(obj(sub).lessons, num(obj(sub).level, 0)),
|
|
150
|
+
stageKey: str(obj(obj(sub).stage).key, ''),
|
|
151
|
+
stageLabel: str(obj(obj(sub).stage).label, ''),
|
|
152
|
+
graduatedLabel: isObj(obj(sub).graduated) ? str(obj(sub).graduated.label, '') : '',
|
|
153
|
+
nextGraduation: typeof obj(sub).nextGraduation === 'number' ? obj(sub).nextGraduation : null,
|
|
154
|
+
minutes: num(obj(sub).minutes, 0),
|
|
155
|
+
tuition: num(obj(sub).tuition, 0),
|
|
156
|
+
gain: num(obj(sub).gain, 0),
|
|
157
|
+
secondaryGain: num(obj(sub).secondaryGain, 0),
|
|
158
|
+
available: obj(sub).available === true,
|
|
159
|
+
affordable: obj(sub).affordable !== false,
|
|
160
|
+
})).filter(sub => sub.key !== ''),
|
|
161
|
+
// 兴趣课:学习页里随时能学的一栏,学完加的是既有的三条属性。
|
|
162
|
+
interests: arr(d.interests).map(entry => ({
|
|
163
|
+
key: str(obj(entry).key, ''),
|
|
164
|
+
label: str(obj(entry).label, '兴趣'),
|
|
165
|
+
emoji: str(obj(entry).emoji, '🎯'),
|
|
166
|
+
traitLabel: str(obj(entry).traitLabel, ''),
|
|
167
|
+
traitEmoji: str(obj(entry).traitEmoji, ''),
|
|
168
|
+
minutes: num(obj(entry).minutes, 0),
|
|
169
|
+
cost: num(obj(entry).cost, 0),
|
|
170
|
+
gain: num(obj(entry).gain, 0),
|
|
171
|
+
blurb: str(obj(entry).blurb, ''),
|
|
172
|
+
times: num(obj(entry).times, 0),
|
|
173
|
+
certificate: str(obj(entry).certificate, ''),
|
|
174
|
+
certificateAfter: num(obj(entry).certificateAfter, 0),
|
|
175
|
+
certified: obj(entry).certified === true,
|
|
176
|
+
available: obj(entry).available === true,
|
|
177
|
+
affordable: obj(entry).affordable === true,
|
|
178
|
+
})).filter(entry => entry.key !== ''),
|
|
179
|
+
stages: arr(d.stages).map(stage => ({
|
|
180
|
+
key: str(obj(stage).key, ''),
|
|
181
|
+
label: str(obj(stage).label, '学段'),
|
|
182
|
+
emoji: str(obj(stage).emoji, '📚'),
|
|
183
|
+
minutes: num(obj(stage).minutes, 0),
|
|
184
|
+
tuition: num(obj(stage).tuition, 0),
|
|
185
|
+
gain: num(obj(stage).gain, 0),
|
|
186
|
+
// B4: the lesson numbers this stage covers (upTo null = no end).
|
|
187
|
+
from: num(obj(stage).from, 0),
|
|
188
|
+
upTo: typeof obj(stage).upTo === 'number' ? obj(stage).upTo : null,
|
|
189
|
+
// Which courses this stage teaches — empty on an old host, in which
|
|
190
|
+
// case the panel shows every subject rather than none.
|
|
191
|
+
subjects: arr(obj(stage).subjects).filter(key => typeof key === 'string'),
|
|
192
|
+
// The school ladder: a stage with `unlocked === false` is gated behind
|
|
193
|
+
// finishing the previous one, and says by how much.
|
|
194
|
+
unlocked: obj(stage).unlocked !== false,
|
|
195
|
+
progress: isObj(obj(stage).progress) ? {
|
|
196
|
+
done: num(obj(stage).progress.done, 0),
|
|
197
|
+
need: num(obj(stage).progress.need, 0),
|
|
198
|
+
label: str(obj(stage).progress.label, ''),
|
|
199
|
+
} : null,
|
|
200
|
+
})).filter(stage => stage.key !== ''),
|
|
201
|
+
trips: arr(d.trips).map(trip => ({
|
|
202
|
+
key: str(obj(trip).key, ''),
|
|
203
|
+
label: str(obj(trip).label, '目的地'),
|
|
204
|
+
emoji: str(obj(trip).emoji, '🧳'),
|
|
205
|
+
minutes: num(obj(trip).minutes, 0),
|
|
206
|
+
cost: num(obj(trip).cost, 0),
|
|
207
|
+
happiness: num(obj(trip).happiness, 0),
|
|
208
|
+
// What the destination can bring back — the far trips advertise it.
|
|
209
|
+
souvenirCount: num(obj(trip).souvenirCount, 0),
|
|
210
|
+
bestRarity: str(obj(trip).bestRarity, ''),
|
|
211
|
+
bestRarityEmoji: str(obj(trip).bestRarityEmoji, ''),
|
|
212
|
+
affordable: obj(trip).affordable === true,
|
|
213
|
+
available: obj(trip).available === true,
|
|
214
|
+
})).filter(trip => trip.key !== ''),
|
|
215
|
+
// 家当: owned and worn, never counted. An old host sends none.
|
|
216
|
+
dress: arr(d.dress).map(entry => ({
|
|
217
|
+
key: str(obj(entry).key, ''),
|
|
218
|
+
label: str(obj(entry).label, '装扮'),
|
|
219
|
+
emoji: str(obj(entry).emoji, '👕'),
|
|
220
|
+
price: num(obj(entry).price, 0),
|
|
221
|
+
level: num(obj(entry).level, 1),
|
|
222
|
+
slot: str(obj(entry).slot, ''),
|
|
223
|
+
slotLabel: str(obj(entry).slotLabel, ''),
|
|
224
|
+
blurb: str(obj(entry).blurb, ''),
|
|
225
|
+
owned: obj(entry).owned === true,
|
|
226
|
+
worn: obj(entry).worn === true,
|
|
227
|
+
unlocked: obj(entry).unlocked !== false,
|
|
228
|
+
})).filter(entry => entry.key !== ''),
|
|
229
|
+
shop: arr(d.shop).map(item => ({
|
|
230
|
+
key: str(obj(item).key, ''),
|
|
231
|
+
label: str(obj(item).label, '物品'),
|
|
232
|
+
emoji: str(obj(item).emoji, '📦'),
|
|
233
|
+
price: num(obj(item).price, 0),
|
|
234
|
+
kind: str(obj(item).kind, 'food'),
|
|
235
|
+
tier: typeof obj(item).tier === 'number' ? obj(item).tier : null,
|
|
236
|
+
// 家当 fields: a dress item is owned (not counted) or waits for a level.
|
|
237
|
+
level: typeof obj(item).level === 'number' ? obj(item).level : null,
|
|
238
|
+
owned: obj(item).owned === true,
|
|
239
|
+
worn: obj(item).worn === true,
|
|
240
|
+
unlocked: obj(item).unlocked !== false,
|
|
241
|
+
blurb: str(obj(item).blurb, ''),
|
|
242
|
+
affordable: obj(item).affordable === true,
|
|
243
|
+
needed: obj(item).needed === true,
|
|
244
|
+
})).filter(item => item.key !== ''),
|
|
245
|
+
inventory: obj(d.inventory),
|
|
246
|
+
daily: {
|
|
247
|
+
canSignIn: obj(d.daily).canSignIn === true,
|
|
248
|
+
signInDay: num(obj(d.daily).signInDay, 1),
|
|
249
|
+
signInTotal: num(obj(d.daily).signInTotal, 0),
|
|
250
|
+
cycle: num(obj(d.daily).cycle, 12),
|
|
251
|
+
unclaimed: num(obj(d.daily).unclaimed, 0),
|
|
252
|
+
onlineMinutes: num(obj(d.daily).onlineMinutes, 0),
|
|
253
|
+
},
|
|
254
|
+
// 新到旧;老宿主没有 diary 时是空数组,面板不显示这一栏。
|
|
255
|
+
diary: arr(d.diary).map(entry => ({
|
|
256
|
+
day: str(obj(entry).day, ''),
|
|
257
|
+
text: str(obj(entry).text, ''),
|
|
258
|
+
})).filter(entry => entry.day !== '' && entry.text !== ''),
|
|
259
|
+
// Which items each care action could spend right now.
|
|
260
|
+
care: (() => {
|
|
261
|
+
const out = {}
|
|
262
|
+
const source = obj(d.care)
|
|
263
|
+
for (const action of ['feed', 'bathe', 'play']) {
|
|
264
|
+
out[action] = arr(source[action]).map(entry => ({
|
|
265
|
+
key: str(obj(entry).key, ''),
|
|
266
|
+
label: str(obj(entry).label, '物品'),
|
|
267
|
+
emoji: str(obj(entry).emoji, '📦'),
|
|
268
|
+
default: obj(entry).default === true,
|
|
269
|
+
count: typeof obj(entry).count === 'number' ? obj(entry).count : null,
|
|
270
|
+
satiety: num(obj(entry).satiety, 0),
|
|
271
|
+
happiness: num(obj(entry).happiness, 0),
|
|
272
|
+
cleanliness: num(obj(entry).cleanliness, 0),
|
|
273
|
+
})).filter(entry => entry.key !== '')
|
|
274
|
+
}
|
|
275
|
+
return out
|
|
276
|
+
})(),
|
|
277
|
+
activity: isObj(d.activity) ? {
|
|
278
|
+
kind: str(d.activity.kind, 'work'),
|
|
279
|
+
key: str(d.activity.key, ''),
|
|
280
|
+
label: str(d.activity.label, '外面'),
|
|
281
|
+
emoji: str(d.activity.emoji, '💼'),
|
|
282
|
+
secondsLeft: num(d.activity.secondsLeft, 0),
|
|
283
|
+
progress: num(d.activity.progress, 0),
|
|
284
|
+
} : null,
|
|
285
|
+
canGoOut: d.canGoOut === true,
|
|
286
|
+
timeScale: num(d.timeScale, 1),
|
|
287
|
+
boxStage: isObj(d.boxStage) ? {
|
|
288
|
+
key: str(d.boxStage.key, 'box'),
|
|
289
|
+
label: str(d.boxStage.label, '纸盒'),
|
|
290
|
+
emoji: str(d.boxStage.emoji, '📦'),
|
|
291
|
+
size: num(d.boxStage.size, 58),
|
|
292
|
+
} : { key: 'box', label: '纸盒', emoji: '📦', size: 58 },
|
|
293
|
+
awayBlocked: typeof d.awayBlocked === 'string' ? d.awayBlocked : null,
|
|
294
|
+
// B9: the villager card. Older hosts send none, and the card says so.
|
|
295
|
+
profile: isObj(d.profile) ? {
|
|
296
|
+
personality: isObj(d.profile.personality) ? { label: str(d.profile.personality.label, ''), emoji: str(d.profile.personality.emoji, '') } : null,
|
|
297
|
+
catchphrase: str(d.profile.catchphrase, ''),
|
|
298
|
+
motto: str(d.profile.motto, ''),
|
|
299
|
+
birthday: str(d.profile.birthday, ''),
|
|
300
|
+
zodiac: isObj(d.profile.zodiac) ? { label: str(d.profile.zodiac.label, ''), emoji: str(d.profile.zodiac.emoji, '') } : null,
|
|
301
|
+
counts: {
|
|
302
|
+
days: num(obj(d.profile.counts).days, 0),
|
|
303
|
+
certificates: num(obj(d.profile.counts).certificates, 0),
|
|
304
|
+
souvenirs: num(obj(d.profile.counts).souvenirs, 0),
|
|
305
|
+
graduations: num(obj(d.profile.counts).graduations, 0),
|
|
306
|
+
},
|
|
307
|
+
} : null,
|
|
308
|
+
// 加冕: the forms and how close the pig is. Older hosts send none.
|
|
309
|
+
forms: isObj(d.forms) ? {
|
|
310
|
+
current: typeof d.forms.current === 'string' ? d.forms.current : null,
|
|
311
|
+
forms: arr(d.forms.forms).map(function (raw) {
|
|
312
|
+
var f = obj(raw)
|
|
313
|
+
return {
|
|
314
|
+
key: str(f.key, ''), label: str(f.label, ''), emoji: str(f.emoji, '👑'), art: str(f.art, ''),
|
|
315
|
+
current: f.current === true, ready: f.ready === true,
|
|
316
|
+
requirements: arr(f.requirements).map(function (row) {
|
|
317
|
+
var r = obj(row)
|
|
318
|
+
return { key: str(r.key, ''), label: str(r.label, ''), have: num(r.have, 0), need: num(r.need, 0), met: r.met === true }
|
|
319
|
+
}),
|
|
320
|
+
}
|
|
321
|
+
}),
|
|
322
|
+
} : null,
|
|
323
|
+
// B6: what the pig calls its owner, and 免打扰. Older hosts send neither.
|
|
324
|
+
dialogue: {
|
|
325
|
+
ownerName: str(obj(d.dialogue).ownerName, '主人'),
|
|
326
|
+
quiet: obj(d.dialogue).quiet === true,
|
|
327
|
+
},
|
|
328
|
+
pending: arr(d.pending).filter(e => isObj(e) && typeof e.at === 'number').map(e => ({
|
|
329
|
+
id: num(e.id, 0),
|
|
330
|
+
kind: str(e.kind, ''),
|
|
331
|
+
text: str(e.text, ''),
|
|
332
|
+
at: e.at,
|
|
333
|
+
replies: arr(e.replies).filter(label => typeof label === 'string'),
|
|
334
|
+
})),
|
|
335
|
+
maxHealth: num(d.maxHealth, 5),
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function normalizeActions(raw) {
|
|
340
|
+
var source = obj(raw)
|
|
341
|
+
var out = {}
|
|
342
|
+
for (var i = 0; i < MODES.length; i += 1) {
|
|
343
|
+
var key = MODES[i]
|
|
344
|
+
var entry = obj(source[key])
|
|
345
|
+
out[key] = {
|
|
346
|
+
ready: entry.ready !== false,
|
|
347
|
+
waitSeconds: num(entry.waitSeconds, 0),
|
|
348
|
+
blocked: typeof entry.blocked === 'string' ? entry.blocked : null,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return out
|
|
352
|
+
}
|