mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
Adv 2.0 L4e reader flip (read-only sites): house state via loadHouseState
dnd_rest long-rest eligibility, dnd_sheet housing display, and petShouldArrive / mistyHousingHint now source house state from player_meta via loadHouseState(userID) instead of *AdventureCharacter. petShouldArrive, mistyHousingHint, and renderDnDSheet gained a HouseState parameter; scheduler and Misty NPC callers load it. Reader flip inside adventure_housing.go itself stays deferred — its mutation paths bundle with cutting AdvCharacter writes post-soak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -244,7 +244,8 @@ func (p *AdventurePlugin) resolveMisty(ctx MessageContext, char *AdventureCharac
|
||||
reply := mistyAcceptLines[rand.IntN(len(mistyAcceptLines))]
|
||||
|
||||
// Housing hint (fires once after 2+ encounters)
|
||||
hint := mistyHousingHint(char)
|
||||
mistyHouse, _ := loadHouseState(char.UserID)
|
||||
hint := mistyHousingHint(char, mistyHouse)
|
||||
if hint != "" {
|
||||
reply += "\n\n_" + hint + "_"
|
||||
}
|
||||
|
||||
@@ -187,13 +187,15 @@ func petDeathText(char *AdventureCharacter) string {
|
||||
// ── Pet Arrival Flow ───────────────────────────────────────────────────────
|
||||
|
||||
// petShouldArrive checks if pet arrival should trigger.
|
||||
// Fires randomly after Tier 1 house upgrade is complete.
|
||||
func petShouldArrive(char *AdventureCharacter) bool {
|
||||
// Fires randomly after Tier 1 house upgrade is complete. Housing state
|
||||
// comes from player_meta via loadHouseState (L4e reader flip); pet flags
|
||||
// still live on AdvCharacter during the soak window.
|
||||
func petShouldArrive(char *AdventureCharacter, house HouseState) bool {
|
||||
if char.PetArrived {
|
||||
return false
|
||||
}
|
||||
// Pet arrives after Tier 1 (Livable) upgrade = HouseTier >= 2
|
||||
if char.HouseTier < 2 {
|
||||
if house.Tier < 2 {
|
||||
return false
|
||||
}
|
||||
// Pet was chased away and reactivated via Misty — allow re-arrival
|
||||
@@ -396,9 +398,10 @@ func petCheckSupplyShopUnlock(char *AdventureCharacter) bool {
|
||||
// ── Misty Housing Hint ─────────────────────────────────────────────────────
|
||||
|
||||
// mistyHousingHint returns a hint about housing/pets if conditions are met.
|
||||
// Fires once, after 2+ Misty encounters, player has no house.
|
||||
func mistyHousingHint(char *AdventureCharacter) string {
|
||||
if char.HasHouse() || char.MistyEncounterCount < 2 {
|
||||
// Fires once, after 2+ Misty encounters, player has no house. Housing
|
||||
// state comes from player_meta (L4e reader flip).
|
||||
func mistyHousingHint(char *AdventureCharacter, house HouseState) string {
|
||||
if house.HasHouse() || char.MistyEncounterCount < 2 {
|
||||
return ""
|
||||
}
|
||||
if char.MistyDonatedCount > 0 {
|
||||
|
||||
@@ -214,37 +214,37 @@ func TestPetDeathText_NoPet(t *testing.T) {
|
||||
// ── Pet Arrival Logic ──────────────────────────────────────────────────────
|
||||
|
||||
func TestPetShouldArrive_NoHouse(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 0}
|
||||
if petShouldArrive(char) {
|
||||
char := &AdventureCharacter{}
|
||||
if petShouldArrive(char, HouseState{Tier: 0}) {
|
||||
t.Error("should not arrive without house")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_BaseHouseOnly(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 1} // Base house, not yet Livable
|
||||
if petShouldArrive(char) {
|
||||
char := &AdventureCharacter{}
|
||||
if petShouldArrive(char, HouseState{Tier: 1}) { // Base house, not yet Livable
|
||||
t.Error("should not arrive with only base house (need tier 2+)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_AlreadyArrived(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 2, PetArrived: true}
|
||||
if petShouldArrive(char) {
|
||||
char := &AdventureCharacter{PetArrived: true}
|
||||
if petShouldArrive(char, HouseState{Tier: 2}) {
|
||||
t.Error("should not trigger if pet already arrived")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_ChasedAway(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 2, PetChasedAway: true}
|
||||
if petShouldArrive(char) {
|
||||
char := &AdventureCharacter{PetChasedAway: true}
|
||||
if petShouldArrive(char, HouseState{Tier: 2}) {
|
||||
t.Error("should not trigger if pet was chased away (and not reactivated)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPetShouldArrive_Reactivated(t *testing.T) {
|
||||
char := &AdventureCharacter{HouseTier: 2, PetChasedAway: true, PetReactivated: true}
|
||||
char := &AdventureCharacter{PetChasedAway: true, PetReactivated: true}
|
||||
// With reactivation, should always return true
|
||||
if !petShouldArrive(char) {
|
||||
if !petShouldArrive(char, HouseState{Tier: 2}) {
|
||||
t.Error("reactivated pet should always trigger arrival")
|
||||
}
|
||||
}
|
||||
@@ -284,21 +284,21 @@ func TestPetMorningEvent_NoPet(t *testing.T) {
|
||||
|
||||
func TestMistyHousingHint_NoEncounters(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 0}
|
||||
if mistyHousingHint(char) != "" {
|
||||
if mistyHousingHint(char, HouseState{}) != "" {
|
||||
t.Error("should not hint with 0 encounters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMistyHousingHint_HasHouse(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 3, HouseTier: 1}
|
||||
if mistyHousingHint(char) != "" {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 3}
|
||||
if mistyHousingHint(char, HouseState{Tier: 1}) != "" {
|
||||
t.Error("should not hint if player has house")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMistyHousingHint_Donated(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 2, MistyDonatedCount: 1}
|
||||
hint := mistyHousingHint(char)
|
||||
hint := mistyHousingHint(char, HouseState{})
|
||||
if !strings.Contains(hint, "house") {
|
||||
t.Errorf("donated player hint should mention house, got: %q", hint)
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func TestMistyHousingHint_Donated(t *testing.T) {
|
||||
|
||||
func TestMistyHousingHint_NotDonated(t *testing.T) {
|
||||
char := &AdventureCharacter{MistyEncounterCount: 2, MistyDonatedCount: 0}
|
||||
hint := mistyHousingHint(char)
|
||||
hint := mistyHousingHint(char, HouseState{})
|
||||
if hint != "Thank you for your time." {
|
||||
t.Errorf("non-donor hint should be dismissive, got: %q", hint)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,8 @@ func (p *AdventurePlugin) sendMorningDMs() {
|
||||
}
|
||||
|
||||
// Pet arrival check (fires before normal morning DM)
|
||||
if petShouldArrive(&char) {
|
||||
house, _ := loadHouseState(char.UserID)
|
||||
if petShouldArrive(&char, house) {
|
||||
p.petArrivalDM(char.UserID)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -126,8 +126,11 @@ func (p *AdventurePlugin) handleDnDLongRest(ctx MessageContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Eligibility: housing OR pay inn fee.
|
||||
hasHousing := advChar.HouseTier > 0
|
||||
// Eligibility: housing OR pay inn fee. Housing read flips to
|
||||
// player_meta via loadHouseState (L4e reader flip); falls back to
|
||||
// adventure_characters during the soak window.
|
||||
house, _ := loadHouseState(ctx.Sender)
|
||||
hasHousing := house.Tier > 0
|
||||
innPaid := false
|
||||
if !hasHousing {
|
||||
if p.euro == nil || !p.euro.Debit(ctx.Sender, float64(dndInnCost), "dnd_inn_long_rest") {
|
||||
|
||||
@@ -63,11 +63,12 @@ func (p *AdventurePlugin) handleDnDSheetCmd(ctx MessageContext) error {
|
||||
equip, _ := loadAdvEquipment(ctx.Sender)
|
||||
treasures, _ := loadAdvTreasureBonuses(ctx.Sender)
|
||||
meta, _ := loadPlayerMeta(ctx.Sender)
|
||||
house, _ := loadHouseState(ctx.Sender)
|
||||
|
||||
return p.SendDM(ctx.Sender, renderDnDSheet(c, advChar, meta, equip, treasures))
|
||||
return p.SendDM(ctx.Sender, renderDnDSheet(c, advChar, meta, house, equip, treasures))
|
||||
}
|
||||
|
||||
func renderDnDSheet(c *DnDCharacter, adv *AdventureCharacter, meta *PlayerMeta, equip map[EquipmentSlot]*AdvEquipment, treasures []AdvTreasureBonus) string {
|
||||
func renderDnDSheet(c *DnDCharacter, adv *AdventureCharacter, meta *PlayerMeta, house HouseState, equip map[EquipmentSlot]*AdvEquipment, treasures []AdvTreasureBonus) string {
|
||||
ri, _ := raceInfo(c.Race)
|
||||
ci, _ := classInfo(c.Class)
|
||||
mods := c.Modifiers()
|
||||
@@ -174,8 +175,8 @@ func renderDnDSheet(c *DnDCharacter, adv *AdventureCharacter, meta *PlayerMeta,
|
||||
if adv.PetName != "" {
|
||||
b.WriteString(fmt.Sprintf(" Pet: %s the %s (lv %d)\n", adv.PetName, adv.PetType, adv.PetLevel))
|
||||
}
|
||||
if adv.HouseTier > 0 {
|
||||
b.WriteString(fmt.Sprintf(" Housing: tier %d\n", adv.HouseTier))
|
||||
if house.Tier > 0 {
|
||||
b.WriteString(fmt.Sprintf(" Housing: tier %d\n", house.Tier))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ func TestSheet_ShowsSubclassWhenChosen(t *testing.T) {
|
||||
STR: 16, DEX: 12, CON: 14, INT: 8, WIS: 10, CHA: 10,
|
||||
Subclass: SubclassBattleMaster,
|
||||
}
|
||||
out := renderDnDSheet(c, nil, nil, nil, nil)
|
||||
out := renderDnDSheet(c, nil, nil, HouseState{}, nil, nil)
|
||||
if !strings.Contains(out, "Battle Master") {
|
||||
t.Errorf("sheet missing subclass name:\n%s", out)
|
||||
}
|
||||
@@ -320,7 +320,7 @@ func TestSheet_PromptsWhenUnchosenAtL5(t *testing.T) {
|
||||
Level: 5, HPMax: 40, HPCurrent: 40, ArmorClass: 16,
|
||||
STR: 16, DEX: 12, CON: 14, INT: 8, WIS: 10, CHA: 10,
|
||||
}
|
||||
out := renderDnDSheet(c, nil, nil, nil, nil)
|
||||
out := renderDnDSheet(c, nil, nil, HouseState{}, nil, nil)
|
||||
if !strings.Contains(out, "!subclass") {
|
||||
t.Errorf("sheet should nudge `!subclass` when unchosen at L5:\n%s", out)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user