mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-18 01:42:42 +00:00
adventure: tell Pete when a story-grade treasure is found
A treasure that earns a public room announce now also files a treasure_found fact, so Pete can count it on the finder's trophy case. The emit rides inside announceTreasureToRoom, reusing the RoomAnnounce != "" gate as the newsworthiness filter — a copper-piece pickup never becomes news, and a reversed auto-swap never emits, because the announce it shares is cancelled on undo. The realm's first finder of a given treasure is a priority hoard; a later finder of the same item is a bulletin, keyed on the treasure across the realm via claimRealmFirst, the same first/repeat split zone_first uses. The item name rides in stakes and the tier-derived rarity in outcome. treasure_found is a new event_type, so Pete's ingest must deploy first or the first finds park on the retry ladder forever.
This commit is contained in:
@@ -1383,6 +1383,10 @@ func (p *AdventurePlugin) announceTreasureToRoom(char *AdventureCharacter, def *
|
|||||||
if def == nil || def.RoomAnnounce == "" {
|
if def == nil || def.RoomAnnounce == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// The same story-grade gate feeds Pete's trophy case. Emit before the
|
||||||
|
// games-room check so a find is still recorded as news even when no room is
|
||||||
|
// configured to announce it in.
|
||||||
|
emitTreasureFound(char.UserID, def, loc)
|
||||||
gr := gamesRoom()
|
gr := gamesRoom()
|
||||||
if gr == "" {
|
if gr == "" {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -336,3 +336,64 @@ func emitZoneClearNews(userID id.UserID, exp *Expedition) {
|
|||||||
OccurredAt: ts,
|
OccurredAt: ts,
|
||||||
}, userID, "")
|
}, userID, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// treasureRarityWord maps a treasure def's tier to the rarity adjective Pete
|
||||||
|
// weaves into a find's dispatch. Story-grade treasures are typically tier 5, so
|
||||||
|
// "legendary" is the common case; the lower tiers are here for completeness.
|
||||||
|
func treasureRarityWord(tier int) string {
|
||||||
|
switch {
|
||||||
|
case tier >= 5:
|
||||||
|
return "legendary"
|
||||||
|
case tier == 4:
|
||||||
|
return "epic"
|
||||||
|
case tier == 3:
|
||||||
|
return "rare"
|
||||||
|
case tier == 2:
|
||||||
|
return "uncommon"
|
||||||
|
default:
|
||||||
|
return "common"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitTreasureFound files a story-grade treasure find. Only treasures carrying a
|
||||||
|
// RoomAnnounce string reach here — the same gate that earns them a public moment
|
||||||
|
// — so a copper-piece pickup never becomes news. The realm's first finder of a
|
||||||
|
// given treasure is a PRIORITY hoard; a later finder of the same item is a
|
||||||
|
// BULLETIN, the first/repeat split zone_first already uses. Character name only;
|
||||||
|
// no-op unless the seam is enabled.
|
||||||
|
//
|
||||||
|
// treasure_found is an event_type Pete's ingest must already know: an unknown
|
||||||
|
// type 400s, retries to the cap, then parks the bulletin forever. Deploy Pete
|
||||||
|
// first.
|
||||||
|
func emitTreasureFound(userID id.UserID, def *AdvTreasureDef, loc *AdvLocation) {
|
||||||
|
if !peteclient.Enabled() || !newsEmissionOn() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if def == nil || loc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Claim the realm-first BEFORE the name guard, so an unnamed straggler's
|
||||||
|
// genuine first find still seeds the ledger and the next finder isn't
|
||||||
|
// mis-billed as the first-ever. Mirrors emitZoneClearNews.
|
||||||
|
tier := "bulletin"
|
||||||
|
if claimRealmFirst("treasure", def.Key) {
|
||||||
|
tier = "priority"
|
||||||
|
}
|
||||||
|
name := charName(userID)
|
||||||
|
if name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts := nowUnix()
|
||||||
|
disc := fmt.Sprintf("%s:%d", def.Key, ts)
|
||||||
|
emitFact(peteclient.Fact{
|
||||||
|
GUID: fmt.Sprintf("treasure_found:%s:%s:%d", eventToken(userID, disc), def.Key, ts),
|
||||||
|
EventType: "treasure_found",
|
||||||
|
Tier: tier,
|
||||||
|
Subject: name,
|
||||||
|
Zone: loc.Name,
|
||||||
|
Level: charLevel(userID),
|
||||||
|
Stakes: def.Name,
|
||||||
|
Outcome: treasureRarityWord(def.Tier),
|
||||||
|
OccurredAt: ts,
|
||||||
|
}, userID, "")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package plugin
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -193,6 +194,71 @@ func TestEmitZoneClearTaxonomy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestEmitTreasureFound: a story-grade find carries the item name and rarity,
|
||||||
|
// and the realm's first finder of a given treasure is billed a PRIORITY hoard
|
||||||
|
// while a later finder of the same item is a BULLETIN — the same first/repeat
|
||||||
|
// split zone_first uses, but keyed on the treasure across the whole realm.
|
||||||
|
func TestEmitTreasureFound(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
db.Close()
|
||||||
|
if err := db.Init(dir); err != nil {
|
||||||
|
t.Fatalf("db.Init: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(db.Close)
|
||||||
|
enablePeteSeam(t)
|
||||||
|
|
||||||
|
db.Exec("seed finder", `INSERT INTO player_meta (user_id, display_name) VALUES (?, ?)`, "@zapp:x", "Zapp")
|
||||||
|
db.Exec("seed second finder", `INSERT INTO player_meta (user_id, display_name) VALUES (?, ?)`, "@kif:x", "Kif")
|
||||||
|
|
||||||
|
def := &AdvTreasureDef{Key: "thunderfury", Name: "Thunderfury, Blessed Blade of the Windseeker",
|
||||||
|
Tier: 5, RoomAnnounce: "x got Thunderfury."}
|
||||||
|
loc := &AdvLocation{Name: "The Abyssal Maw"}
|
||||||
|
|
||||||
|
emitTreasureFound(id.UserID("@zapp:x"), def, loc) // realm-first
|
||||||
|
emitTreasureFound(id.UserID("@kif:x"), def, loc) // same item, later finder
|
||||||
|
|
||||||
|
if got := queuedCount(t, "treasure_found:%"); got != 2 {
|
||||||
|
t.Fatalf("treasure_found queued = %d, want 2", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull both payloads and check the taxonomy split plus the carried fields.
|
||||||
|
rows, err := db.Get().Query(`SELECT payload FROM pete_emit_queue WHERE guid LIKE 'treasure_found:%'`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
tiers := map[string]int{}
|
||||||
|
for rows.Next() {
|
||||||
|
var payload string
|
||||||
|
if err := rows.Scan(&payload); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var f map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(payload), &f); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tiers[f["tier"].(string)]++
|
||||||
|
if f["stakes"] != def.Name {
|
||||||
|
t.Errorf("stakes = %v, want the item name", f["stakes"])
|
||||||
|
}
|
||||||
|
if f["zone"] != "The Abyssal Maw" {
|
||||||
|
t.Errorf("zone = %v, want the location", f["zone"])
|
||||||
|
}
|
||||||
|
if f["outcome"] != "legendary" {
|
||||||
|
t.Errorf("outcome = %v, want legendary for a tier-5 find", f["outcome"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tiers["priority"] != 1 || tiers["bulletin"] != 1 {
|
||||||
|
t.Errorf("tier split = %v, want one priority (realm-first) and one bulletin (repeat)", tiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ledger is seeded, so a later live find of the same treasure won't
|
||||||
|
// mis-announce as the first-ever.
|
||||||
|
if claimRealmFirst("treasure", "thunderfury") {
|
||||||
|
t.Error("realm-first ledger not seeded for the treasure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestNewsEmissionKillSwitch: the runtime flag defaults on and persists a flip.
|
// TestNewsEmissionKillSwitch: the runtime flag defaults on and persists a flip.
|
||||||
func TestNewsEmissionKillSwitch(t *testing.T) {
|
func TestNewsEmissionKillSwitch(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|||||||
Reference in New Issue
Block a user