mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
N5/D1a: journal pages — the Hollow King campaign collectible
First slice of the N5 story layer. Adds a 24-page serialized campaign threaded through the zones as collectible journal pages. - Storage: one journal_pages INTEGER bitmask on player_meta (bit i == page i+1). DEFAULT 0 is correct for every existing row, so no bootstrap. Grants are an atomic bitwise-OR (INSERT ... ON CONFLICT DO UPDATE SET journal_pages = journal_pages | excluded.journal_pages) so a page found mid-expedition can't be lost to a stale character save. Journal pages are therefore grant-only + overlay-read, never in the bulk upsert. - Drop seam: elite kills roll a page (22%, tunable) in dropZoneLoot, gated on isElite — bosses get epilogues (D1b), not pages. Not on SimulateCombat's path, so TestCombatCharacterization is byte-identical. - Viewer: !adventure journal renders found pages in story order with runs of missing pages collapsed to a single "…". - Catalog + bitmask helpers + render live in the new adventure_flavor_campaign.go; the pages are in-world found artifacts, not TwinBee's voice. Golden byte-identical; go test ./... green (incl. a real-DB round-trip that pins the atomic OR + overlay read). Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
@@ -386,6 +386,41 @@ func upsertPlayerMetaPet2State(userID id.UserID, s PetState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// loadJournalPages reads the Hollow King campaign page bitmask (N5/D1). A
|
||||
// missing row means no pages found yet.
|
||||
func loadJournalPages(userID id.UserID) (int64, error) {
|
||||
var mask int64
|
||||
err := db.Get().QueryRow(
|
||||
`SELECT journal_pages FROM player_meta WHERE user_id = ?`,
|
||||
string(userID),
|
||||
).Scan(&mask)
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return mask, nil
|
||||
}
|
||||
|
||||
// grantJournalPageDB sets the bit for one page atomically. The OR against the
|
||||
// stored value (not a read-modify-write of the in-memory character) means a
|
||||
// page granted mid-expedition survives a concurrent character save, and the
|
||||
// call is idempotent — re-granting a found page is a no-op.
|
||||
func grantJournalPageDB(userID id.UserID, page int) error {
|
||||
if page < 1 {
|
||||
return nil
|
||||
}
|
||||
bit := int64(1) << (page - 1)
|
||||
_, err := db.Get().Exec(
|
||||
`INSERT INTO player_meta (user_id, journal_pages) VALUES (?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
journal_pages = journal_pages | excluded.journal_pages`,
|
||||
string(userID), bit,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// HouseState is the in-memory mirror of player_meta's house_* columns. Phase
|
||||
// L4e ports housing/mortgage off AdvCharacter (gogobee_legacy_migration.md
|
||||
// §6.5). All six fields mutate together at known sites (purchase, payoff,
|
||||
@@ -1281,6 +1316,9 @@ func applyPlayerMetaOverlay(c *AdventureCharacter) {
|
||||
c.Pet2ChasedAway = s.ChasedAway
|
||||
c.Pet2Reactivated = s.Reactivated
|
||||
}
|
||||
if mask, err := loadJournalPages(uid); err == nil {
|
||||
c.JournalPages = mask
|
||||
}
|
||||
if s, err := loadHouseState(uid); err == nil {
|
||||
c.HouseTier = s.Tier
|
||||
c.HouseLoanBalance = s.LoanBalance
|
||||
|
||||
Reference in New Issue
Block a user