mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Four anchor holidays (Hallowtide, Midwinter Feast, Sweethearts' Revel,
First Bloom) each get a 7-day window (anchor ±3 days) that layers three
things on the world, all reusing existing machinery:
1. A themed Omen that overrides the weekly rotation. Reuses the B3 omen
effect fields, so the non-combat rule holds; kept behind activeOmen's
simOmenDisabled guard (and activeSeason honours it too) so no season
path can reach the balance sim or move the golden.
2. A curated curio shelf at Luigi's — existing registry items rotated to
the front of dailyCuriosStock, so no net-new power enters the economy.
Off-season output is byte-identical to before.
3. A themed road visitor via the ambient seam — a season_visitor event
that leaves a sellable keepsake + coin gift. No combat: the ambient
seam still never opens a fight.
Pure function of the UTC date + anchor calendar — no schema, ticker, or
persistence, same discipline as the Omen and holiday calendar. Season
banner rides the existing morning DM (no net-new scheduled message).
go test ./internal/plugin ./internal/db green; combat golden byte-identical.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
121 lines
4.0 KiB
Go
121 lines
4.0 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func date(y int, m time.Month, d int) time.Time {
|
|
return time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
|
|
}
|
|
|
|
// TestSeasonForDate_ActiveOnAnchor — every season resolves on its own anchor day
|
|
// and both window edges (±seasonHalfWidth), and is dark one day past each edge.
|
|
func TestSeasonForDate_ActiveOnAnchor(t *testing.T) {
|
|
for _, s := range seasonTable {
|
|
a := s.Anchor(2026)
|
|
for _, off := range []int{-seasonHalfWidth, 0, seasonHalfWidth} {
|
|
got, ok := seasonForDate(a.AddDate(0, 0, off))
|
|
if !ok || got.Key != s.Key {
|
|
t.Errorf("%s: day offset %d → (%v, %q), want (true, %q)",
|
|
s.Key, off, ok, got.Key, s.Key)
|
|
}
|
|
}
|
|
for _, off := range []int{-seasonHalfWidth - 1, seasonHalfWidth + 1} {
|
|
if got, ok := seasonForDate(a.AddDate(0, 0, off)); ok {
|
|
t.Errorf("%s: day offset %d should be dark, got %q", s.Key, off, got.Key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSeasonForDate_WindowsDisjoint — no calendar day resolves to two seasons, so
|
|
// the first-match order in seasonForDate never hides overlapping content.
|
|
func TestSeasonForDate_WindowsDisjoint(t *testing.T) {
|
|
for y := 2024; y <= 2030; y++ {
|
|
day := date(y, time.January, 1)
|
|
end := date(y+1, time.January, 1)
|
|
for day.Before(end) {
|
|
matches := 0
|
|
for _, s := range seasonTable {
|
|
for _, yr := range []int{day.Year() - 1, day.Year(), day.Year() + 1} {
|
|
a := s.Anchor(yr)
|
|
if !day.Before(a.AddDate(0, 0, -seasonHalfWidth)) &&
|
|
!day.After(a.AddDate(0, 0, seasonHalfWidth)) {
|
|
matches++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if matches > 1 {
|
|
t.Fatalf("%s resolves to %d seasons (windows must be disjoint)",
|
|
day.Format("2006-01-02"), matches)
|
|
}
|
|
day = day.AddDate(0, 0, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSeasonTable_CuratedItemsExist — every curated curio ID is a real registry
|
|
// item, so a season's shelf never lists a phantom the buyer can't purchase. Guards
|
|
// against typos and registry edits (mirrors TestTemperMaterialsAreObtainable).
|
|
func TestSeasonTable_CuratedItemsExist(t *testing.T) {
|
|
for _, s := range seasonTable {
|
|
if len(s.CurioIDs) == 0 {
|
|
t.Errorf("%s has no curated curios", s.Key)
|
|
}
|
|
for _, id := range s.CurioIDs {
|
|
if _, ok := magicItemRegistry[id]; !ok {
|
|
t.Errorf("%s curio %q is not in magicItemRegistry", s.Key, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSeasonTable_OmenIsNonCombat — a season's themed omen carries an effect and
|
|
// only touches the non-combat omen levers, so overriding the weekly rotation with
|
|
// it can never move the combat golden or the balance corpus. §E4/§B3.
|
|
func TestSeasonTable_OmenIsNonCombat(t *testing.T) {
|
|
for _, s := range seasonTable {
|
|
o := s.Omen
|
|
hasEffect := o.HarvestYieldBonus > 0 || o.SupplyFreebiePacks > 0 ||
|
|
o.StartMoodBonus > 0 || o.ArenaPayoutMult > 1.0 ||
|
|
o.ConsumableChanceMult > 1.0 || o.ThreatDriftReduce > 0
|
|
if !hasEffect {
|
|
t.Errorf("%s omen has no active effect", s.Key)
|
|
}
|
|
if o.Name == "" || o.TwinBee == "" {
|
|
t.Errorf("%s omen missing player-facing copy", s.Key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSeasonTable_VisitorRewardComplete — every season's road visitor has flavor,
|
|
// a named keepsake, and a positive keepsake value, so applyAmbientEffect always
|
|
// has a real reward to grant.
|
|
func TestSeasonTable_VisitorRewardComplete(t *testing.T) {
|
|
for _, s := range seasonTable {
|
|
v := s.Visitor
|
|
if len(v.FlavorPool) == 0 {
|
|
t.Errorf("%s visitor has no flavor", s.Key)
|
|
}
|
|
if v.Keepsake == "" || v.KeepsakeValue <= 0 {
|
|
t.Errorf("%s visitor keepsake incomplete: %q value %d", s.Key, v.Keepsake, v.KeepsakeValue)
|
|
}
|
|
if v.Weight <= 0 {
|
|
t.Errorf("%s visitor has non-positive weight %d", s.Key, v.Weight)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestActiveSeason_NeutralizedInSim — the balance sim must never observe a season
|
|
// through any path; activeSeason honours simOmenDisabled exactly as activeOmen does.
|
|
func TestActiveSeason_NeutralizedInSim(t *testing.T) {
|
|
prev := simOmenDisabled
|
|
simOmenDisabled = true
|
|
defer func() { simOmenDisabled = prev }()
|
|
if s, ok := activeSeason(); ok {
|
|
t.Fatalf("activeSeason returned %q while simOmenDisabled", s.Key)
|
|
}
|
|
}
|