Compare commits
3 Commits
adventure-
...
a614077cff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a614077cff | ||
|
|
82d1c6ebeb | ||
|
|
10bcc78c51 |
@@ -188,6 +188,8 @@ func advEventMeta(eventType string) (label, emoji string) {
|
||||
return "Pete's duels", "🤝"
|
||||
case "milestone":
|
||||
return "Milestone", "🏅"
|
||||
case "retreat":
|
||||
return "Pulled out", "🎒"
|
||||
}
|
||||
return "Dispatch", "📣"
|
||||
}
|
||||
@@ -375,6 +377,19 @@ func renderAdventure(f AdvFact) (headline, lede string, ok bool) {
|
||||
case "death":
|
||||
return fmt.Sprintf("We lost %s in %s.", f.Subject, f.Zone),
|
||||
fmt.Sprintf("Sad news to pass along: %s fell at level %d in %s. The graveyard's a little fuller tonight. Rest easy.", f.Subject, f.Level, f.Zone), true
|
||||
case "retreat":
|
||||
// An expedition that came apart without killing anyone. Until gogobee
|
||||
// started sending these, the feed had no way to say "it went badly and
|
||||
// everyone lived" — so it never said it, and the classes that retreat
|
||||
// often simply never appeared. Warm, not a failure notice: everyone came
|
||||
// home, and that is the part Pete leads with.
|
||||
howFar := "barely a day in"
|
||||
if f.Count > 1 {
|
||||
howFar = fmt.Sprintf("%d days in", f.Count)
|
||||
}
|
||||
return fmt.Sprintf("%s backed out of %s.", f.Subject, f.Zone),
|
||||
fmt.Sprintf("%s turned around %s — %s got the better of them this time%s, and they made the call to walk out rather than push it. Everybody came home breathing, which is the bit that counts. That dungeon'll still be there next week.",
|
||||
f.Subject, howFar, f.Zone, atLevel), true
|
||||
case "arrival":
|
||||
return fmt.Sprintf("Welcome to the realm, %s!", f.Subject),
|
||||
fmt.Sprintf("A new %s just walked through the gates. Say hello if you see them out there.", f.ClassRace), true
|
||||
|
||||
55
internal/web/adventure_retreat_test.go
Normal file
55
internal/web/adventure_retreat_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"pete/internal/storage"
|
||||
)
|
||||
|
||||
// The retreat bulletin — gogobee's newest event type.
|
||||
//
|
||||
// The failure this guards against is silent and total: handleAdventureIngest
|
||||
// answers an unrecognized event_type with 400, and gogobee's sender treats any
|
||||
// non-2xx as a failure, retries eight times over ~two hours, and then PARKS the
|
||||
// row forever. So a gogobee that emits `retreat` against a Pete that doesn't
|
||||
// know the word doesn't log an error anyone reads — it just quietly drops every
|
||||
// retreat the realm ever files. Pete has to learn the word first, and this test
|
||||
// is what says he has.
|
||||
func TestAdventureIngest_AcceptsRetreat(t *testing.T) {
|
||||
const token = "s3cret-token"
|
||||
s, posted := newAdvServer(t, token)
|
||||
|
||||
f := AdvFact{
|
||||
GUID: "retreat:abc:1000", EventType: "retreat", Tier: "bulletin",
|
||||
Actors: []string{"Brannigan"}, Subject: "Brannigan",
|
||||
Zone: "the Underforge", Level: 12, Count: 3, Outcome: "retreated",
|
||||
OccurredAt: 1000,
|
||||
}
|
||||
if rw := postFact(t, s, token, f); rw.Code != 200 {
|
||||
t.Fatalf("ingest status = %d body=%s — Pete rejected a retreat, so gogobee will "+
|
||||
"retry it eight times and park it forever", rw.Code, rw.Body.String())
|
||||
}
|
||||
|
||||
got, err := storage.GetStoryByGUID("retreat:abc:1000")
|
||||
if err != nil || got == nil {
|
||||
t.Fatalf("retreat was accepted but not stored: %v", err)
|
||||
}
|
||||
// It has to actually say what happened, in Pete's voice, using only the
|
||||
// supplied facts.
|
||||
body := got.Headline + " " + got.Lede
|
||||
for _, want := range []string{"Brannigan", "the Underforge"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("rendered retreat is missing %q: %q", want, body)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(got.Lede, "3 days in") {
|
||||
t.Errorf("the day count never made it into the copy: %q", got.Lede)
|
||||
}
|
||||
// Bulletin, not priority: a retreat goes in the daily digest, it does not
|
||||
// interrupt the room. Pete announcing every failed run live would be a
|
||||
// firehose — and an unkind one.
|
||||
if len(*posted) != 0 {
|
||||
t.Errorf("a bulletin was posted live: %+v", *posted)
|
||||
}
|
||||
}
|
||||
13
main.go
13
main.go
@@ -267,11 +267,16 @@ func main() {
|
||||
|
||||
// Adapter: the web ingest handler posts priority adventure beats live to
|
||||
// Matrix through the same queue everything else uses (bypasses pacing).
|
||||
// PostNow doesn't consult posting.enabled, so gate here: with posting off,
|
||||
// adventure stays website-only like every other channel (nil poster also
|
||||
// keeps the digest loop from starting).
|
||||
//
|
||||
// Adventure carries its own enable switch and is push-based: facts arrive
|
||||
// from gogobee at ingest, they are not polled, classified or paced, and they
|
||||
// never enter the round-robin rotation. So it is gated on [adventure] alone,
|
||||
// NOT on posting.enabled — that flag governs the RSS pipeline's chatter, and
|
||||
// an operator running "news on the web only, adventure live in the games
|
||||
// room" is a legitimate configuration. A nil poster (adventure off, or no
|
||||
// channel) still keeps both the live beats and the digest loop shut.
|
||||
var advPost web.PriorityPoster
|
||||
if postingEnabled {
|
||||
if cfg.Adventure.Enabled && cfg.Adventure.Channel != "" {
|
||||
advPost = func(p web.AdvPost) {
|
||||
queue.PostNow(poster.QueueItem{
|
||||
GUID: p.GUID,
|
||||
|
||||
Reference in New Issue
Block a user