Repository navigation
feat: add companion tuning controls and desktop pet visibility toggle - #209
issachar-vin wants to merge 1 commit into
Conversation
Adds four related controls for tuning and inspecting the companion, plus the plumbing each one needed. Difficulty multipliers (Settings) - Two independent multipliers: growth (egg incubation, evolution and graduation thresholds) and shop prices. Applied at the consumption sites in CompanionStore rather than to the constant tables, so the two stay independent: graded egg prices derive from a ratio of graduationTotal, and scaling that table in place would let the growth slider move shop pricing. - Persisted in UserDefaults alongside the other Settings preferences, not in CompanionState. Difficulty is a preference, not save data, so it stays out of save export/import and SaveTransfer is untouched. - Applied live: lowering growth re-evaluates evolution and hatching immediately instead of waiting for the next usage poll. - RareCandy.xp is deliberately not scaled; scaling it would cancel against the thresholds and make candy immune to difficulty. - The slider is log-mapped, since the range spans several orders of magnitude and a linear track makes the default unreachable. Values snap to two significant figures, and a position-space window keeps 100% selectable by drag. Spawn egg (Settings) - Releases the current companion and pins the next hatch to a chosen species via the existing pendingHatchID pre-roll. Hatching still follows the normal rules: incubation must be refilled, and shiny, nature and branch still roll at hatch. - buyEgg's discard transition is extracted into resetToFreshEgg so both paths share it; duplicating it invites drift in the easy-to-forget parts (generation bump, representative reconcile). - The base index query now carries localized names in the same request, so the picker shows real names without extra round trips. Snapshots written before that field are treated as expired: lenient decoding kept them readable but nameless, which would have left the list showing bare numbers until the 30-day TTL elapsed. Floating pet animation (Settings) - The pet was capped at 0.4s per frame (2.5fps) while sprites run at 10-16fps natively, with sleep tolerance jitter on top, so it read as stuttering next to the popover. The cap is now a toggle defaulting to smooth; turning it off restores the original battery cap. - minFrameDelay is included in SpriteView's task id, because the frame loop holds the floor captured at start and would otherwise ignore the setting until the next sprite change. - Low-power mode still freezes the pet regardless of this setting. Popover footer - An eye button toggles desktop pet visibility, reading and writing the same floatingPetEnabled property as the Settings switch, so the two cannot diverge. Tests - DifficultyTests covers scaling behaviourally (same token input, different outcome), slider mapping, and egg spawning. Verified by mutation: removing the multipliers or the species pin fails them. - The pet frame-floor guard no longer proves the cap is applied now that it is conditional, so the applied branch is covered separately and the original test notes its narrowed scope. - defect-log.md records the fps cap becoming user-selectable and the task-id trap, so the documented invariant does not go stale.
|
Thanks for this, and in particular for how the tests are written — verifying by mutation rather than by passing (stripping the multipliers fails 8 tests, making the species pin a no-op fails 4) is the right instinct, and noting that the spawn stub could have let a test pass vacuously is the kind of detail most PRs don't catch. I'm closing it, for three separate reasons. The PR is too large. Four independent controls in one change means I can't take the part I want without the parts I don't. Spawn egg is a no from me, permanently. Letting the user pick the next species removes the randomness, and the randomness is the appeal. Not knowing what hatches, and a legendary being a rare event rather than a menu choice, is what makes the Pokédex worth filling. Pinning the species keeps the incubation cost but removes the reason to care about it. The animation toggle is already resolved. #212 landed and went further: the stutter wasn't only the cap, it was that the cap was applied by holding each frame instead of decimating, so a 55-frame sprite played at 1/8 speed. That's fixed, and the cap is now a user-selectable preset. This part of the PR would undo it. What I would take, as a separate and much smaller PR: the difficulty multipliers. They answer a real request (#226 — light users waiting far too long for progress), and the design is sound — scaling at the consumption sites rather than the constant tables so growth and shop pricing stay independent, keeping the setting in If you open that as its own PR I'll review it promptly. Thanks again for the work. |
Four related controls for tuning and inspecting the companion, plus the plumbing each needed.
Difficulty multipliers (Settings)
Two independent multipliers — growth (egg incubation, evolution, graduation) and shop prices — applied live, no restart.
CompanionStore, not on the constant tables. This matters: graded egg prices derive from a ratio ofgraduationTotal, so scaling that table in place would let the growth slider silently move shop pricing. Consumption-site scaling keeps the two independent and leaves every existing balance test valid.UserDefaultswith the other Settings preferences, not inCompanionState. Difficulty is a preference, not save data — keeping it out means save export/import can't carry someone else's difficulty, andSaveTransferis untouched.RareCandy.xpis deliberately not scaled. Scaling it would cancel against the thresholds and make candy the one thing immune to difficulty.Spawn egg (Settings)
Releases the current companion and pins the next hatch to a chosen species, using the existing
pendingHatchIDpre-roll — no new hatch path.buyEgg's discard transition is extracted intoresetToFreshEggso both paths share it. Duplicating it invites drift in exactly the easy-to-forget parts (generation bump, representative reconcile) that cause async-overwrite bugs.Floating pet animation (Settings)
The pet was capped at 0.4s/frame (2.5fps) while sprites run at 10–16fps natively, with sleep-tolerance jitter on top — so it read as stuttering next to the popover, which runs uncapped.
minFrameDelayis included inSpriteView's.task(id:). The frame loop holds the floor captured at start, so without this the setting would appear dead until the next sprite change.Popover footer
An eye button toggles desktop pet visibility. It reads and writes the same
floatingPetEnabledproperty the Settings switch is bound to, so the two cannot diverge — no mirrored state.Tests
DifficultyTests(30 tests) covers scaling behaviourally — same token input to stores differing only in multiplier, asserting on state transitions rather than displayed strings — plus slider mapping and egg spawning.Verified by mutation rather than by passing: stripping the multipliers out of the seams fails 8 tests, and making the species pin a no-op fails 4. That check mattered — the spawn stub returns a fixed line, so one test could have passed vacuously.
The pet frame-floor guard no longer proves the cap is applied now that it's conditional, so the applied branch is covered separately and the original test notes its narrowed scope rather than reading stronger than it is.
docs/reference/defect-log.mdrecords the fps cap becoming user-selectable and the task-id trap, so the documented invariant doesn't go stale.Notes
LocalAdditionalUsageTests.testHermesAcceptsMillisecondStartedAtfails on this branch, but it also fails on a cleanmain— a timezone-dependent date assertion, unrelated to these changes. Everything else passes (765 tests, 8 skipped), with zero build warnings.hasActivegate) and unchanged here, but Spawn Egg makes egg state far easier to reach, so it surfaces more often.한국어
컴패니언을 조절하고 들여다보기 위한 네 가지 컨트롤과, 각각에 필요한 배선을 더한다.
난이도 배율 (설정)
성장(알 인큐베이션·진화·졸업)과 상점 가격, 서로 독립인 두 배율. 재시작 없이 즉시 반영된다.
CompanionStore의 소비 지점에서 곱한다. 이게 중요한 이유: 등급 알 가격이graduationTotal의 비율에서 파생되므로, 표를 직접 스케일하면 성장 배율이 상점 가격까지 조용히 끌고 간다. 소비 지점 스케일이라야 두 배율이 독립으로 남고, 기존 밸런스 테스트도 전부 그대로 유효하다.CompanionState가 아니라 다른 설정값들과 함께UserDefaults에 저장한다. 난이도는 진행 상황이 아니라 취향 설정이다 — 세이브에서 빼두면 남의 세이브를 불러올 때 내 난이도가 바뀌는 일이 없고,SaveTransfer는 아예 건드리지 않는다.RareCandy.xp는 의도적으로 스케일하지 않는다. 함께 곱하면 임계와 서로 상쇄돼 사탕만 난이도를 안 타게 된다.알 스폰 (설정)
현재 컴패니언을 놓아주고, 기존
pendingHatchIDpre-roll 을 이용해 다음 부화 종을 고정한다 — 새 부화 경로를 만들지 않는다.buyEgg의 폐기 전이를resetToFreshEgg로 뽑아 두 경로가 공유하게 했다. 복제해 두면 빠뜨리기 쉬운 부분(세대 증가, 대표 종 복구)에서 어긋나고, 그게 비동기 덮어쓰기 결함의 원인이 된다.플로팅 펫 애니메이션 (설정)
펫은 0.4s/프레임(2.5fps)로 묶여 있었는데 스프라이트 원본은 10~16fps 라, 여기에 sleep tolerance 지터까지 겹쳐 캡이 없는 팝오버 옆에 두면 버벅이는 것처럼 보였다.
minFrameDelay를SpriteView의.task(id:)에 포함했다. 프레임 루프는 시작 시점의 하한을 들고 돌기 때문에, 이게 없으면 다음 스프라이트 교체 전까지 설정이 안 먹는 것처럼 보인다.팝오버 푸터
눈 아이콘으로 데스크톱 펫 표시를 토글한다. 설정창 스위치가 묶여 있는 것과 같은
floatingPetEnabled를 읽고 쓰므로 둘이 어긋날 수 없다 — 별도 미러 상태가 없다.테스트
DifficultyTests(30개)는 스케일을 동작으로 검증한다 — 배율만 다른 스토어에 같은 토큰을 넣고, 표시 문자열이 아니라 상태 전이를 단언한다. 슬라이더 매핑과 알 스폰도 함께 덮는다.통과가 아니라 변이(mutation)로 검증했다: 배율을 빼면 8개가, 종 고정을 무력화하면 4개가 깨진다. 이 확인이 실제로 의미가 있었다 — 스폰 스텁은 고정된 라인을 돌려주므로 한 테스트는 그냥 통과해 버릴 수도 있었다.
펫 프레임 하한 가드는 캡이 조건부가 된 지금 "실제로 적용된다"를 더는 증명하지 못한다. 그래서 적용 분기를 따로 덮고, 원래 테스트에는 실제보다 강해 보이지 않도록 범위가 좁아졌음을 명시했다.
docs/reference/defect-log.md에 fps 캡이 사용자 선택이 된 사실과 task-id 함정을 기록해, 문서에 적힌 불변식이 낡지 않게 했다.참고
LocalAdditionalUsageTests.testHermesAcceptsMillisecondStartedAt가 이 브랜치에서 실패하지만, 깨끗한main에서도 똑같이 실패한다 — 타임존에 의존하는 날짜 단언이고 이번 변경과 무관하다. 그 외에는 전부 통과한다(765개, 8개 skip). 빌드 경고 0.hasActive게이트)이고 여기서 바꾸지 않았지만, 알 스폰 때문에 알 상태에 훨씬 쉽게 들어가게 돼서 더 자주 눈에 띈다.