mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Compare commits
2 Commits
6e4928ca17
...
forex-cryp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ea3e42612 | ||
|
|
3ed2e1d8e0 |
@@ -206,6 +206,25 @@ func petShouldArrive(pet PetState, house HouseState) bool {
|
||||
return rand.Float64() < 0.30
|
||||
}
|
||||
|
||||
// maybeRollPetArrivalOnEmerge rolls the pet-arrival check when a player
|
||||
// surfaces from an expedition (voluntary extract, abandon, or a survived
|
||||
// forced extraction) or revives after death. The arrival roll lives on the
|
||||
// emergence seam — not the legacy 08:00 overworld morning DM — because
|
||||
// expedition players are almost never in the overworld at the scheduled hour,
|
||||
// so the morning roll never reached them. Story beat: while the player was
|
||||
// underground, an animal wandered into the empty house looking for food.
|
||||
//
|
||||
// Safe to call unconditionally on any emergence: petShouldArrive gates on
|
||||
// house tier / not-yet-arrived, and petArrivalDM won't clobber an existing
|
||||
// pending interaction.
|
||||
func (p *AdventurePlugin) maybeRollPetArrivalOnEmerge(userID id.UserID) {
|
||||
pet, _ := loadPetState(userID)
|
||||
house, _ := loadHouseState(userID)
|
||||
if petShouldArrive(pet, house) {
|
||||
p.petArrivalDM(userID)
|
||||
}
|
||||
}
|
||||
|
||||
// petArrivalDM sends the initial "there's an animal in your house" DM.
|
||||
func (p *AdventurePlugin) petArrivalDM(userID id.UserID) {
|
||||
// Don't overwrite an existing pending interaction
|
||||
|
||||
@@ -82,6 +82,12 @@ func (p *AdventurePlugin) sendMorningDMs() {
|
||||
if err := p.SendDM(char.UserID, text); err != nil {
|
||||
slog.Error("adventure: failed to send respawn DM", "user", char.UserID, "err", err)
|
||||
}
|
||||
|
||||
// Emergence seam (death case): a player who died underground
|
||||
// "comes home" on respawn. This is the deferred half of the
|
||||
// emergence roll — survived extractions roll at their exit
|
||||
// site; deaths roll here. See maybeRollPetArrivalOnEmerge.
|
||||
p.maybeRollPetArrivalOnEmerge(char.UserID)
|
||||
}
|
||||
|
||||
// Babysitting: pet-care trickle (no harvest actions; safe-rest perk
|
||||
@@ -133,13 +139,12 @@ func (p *AdventurePlugin) sendMorningDMs() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Pet arrival check (fires before normal morning DM)
|
||||
house, _ := loadHouseState(char.UserID)
|
||||
// Pet arrival no longer rolls here. The 08:00 overworld morning DM
|
||||
// is skipped for anyone underground (expedition gate above), so it
|
||||
// never reached expedition players. Arrival now fires on the
|
||||
// emergence seam — see maybeRollPetArrivalOnEmerge, called from the
|
||||
// extract/abandon/forced-extract and respawn paths.
|
||||
pet, _ := loadPetState(char.UserID)
|
||||
if petShouldArrive(pet, house) {
|
||||
p.petArrivalDM(char.UserID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Morning pet event
|
||||
petEvent := petMorningEvent(pet)
|
||||
|
||||
@@ -486,9 +486,14 @@ func (p *AdventurePlugin) expeditionCmdAbandon(ctx MessageContext) error {
|
||||
}
|
||||
_ = retireAllRegionRuns(exp)
|
||||
_ = appendExpeditionLog(exp.ID, exp.CurrentDay, "narrative", "expedition abandoned", "")
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
if err := p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||
"Expedition in **%s** abandoned on Day %d. Supplies are forfeit. The dungeon remembers.",
|
||||
zone.Display, exp.CurrentDay))
|
||||
zone.Display, exp.CurrentDay)); err != nil {
|
||||
return err
|
||||
}
|
||||
// Emergence seam: see maybeRollPetArrivalOnEmerge.
|
||||
p.maybeRollPetArrivalOnEmerge(ctx.Sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
// helper: ensure we don't shadow id.UserID import in test harness.
|
||||
|
||||
@@ -303,6 +303,14 @@ func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error {
|
||||
if err := p.SendDM(uid, body); err != nil {
|
||||
slog.Warn("expedition: send briefing DM", "user", uid, "err", err)
|
||||
}
|
||||
// Emergence seam: a briefing-time forced extraction (starvation /
|
||||
// abyss collapse) surfaces the player alive — roll pet arrival.
|
||||
// Combat/patrol deaths never reach deliverBriefing (the row is
|
||||
// already abandoned), so an abandoned status here means a survived
|
||||
// emergence; those death paths roll on respawn instead.
|
||||
if e.Status == ExpeditionStatusAbandoned {
|
||||
p.maybeRollPetArrivalOnEmerge(uid)
|
||||
}
|
||||
}
|
||||
if err := appendExpeditionLog(e.ID, e.CurrentDay, "briefing",
|
||||
fmt.Sprintf("morning briefing — %.1f SU consumed overnight", burn), line); err != nil {
|
||||
|
||||
@@ -179,7 +179,13 @@ func (p *AdventurePlugin) handleExtractCmd(ctx MessageContext, _ string) error {
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Loot, XP, and coins are kept. The dungeon stays where you left it — `!resume` within 7 days to come back. After %s the expedition expires.",
|
||||
(time.Now().UTC().Add(extractResumeWindow)).Format("2006-01-02 15:04 MST")))
|
||||
return p.SendDM(ctx.Sender, b.String())
|
||||
if err := p.SendDM(ctx.Sender, b.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
// Emergence seam: surfacing from a run is when an animal may have moved
|
||||
// into the empty house.
|
||||
p.maybeRollPetArrivalOnEmerge(ctx.Sender)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ── !resume command ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -122,6 +122,10 @@ const fxHelpText = "**Forex Commands**\n\n" +
|
||||
// ── Command Handlers ────────────────────────────────────────────────────────
|
||||
|
||||
func (p *ForexPlugin) cmdRate(ctx MessageContext, args []string) error {
|
||||
if msg := fxValidateAnalysisArgs(args); msg != "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||
}
|
||||
|
||||
// Check for cross-pair syntax like EUR/USD or USD/JPY
|
||||
if pair := fxParsePair(args, false); pair != nil {
|
||||
return p.cmdPairRateOrReport(ctx, pair, false)
|
||||
@@ -277,6 +281,9 @@ func (p *ForexPlugin) fxPerUSD(cur string, fiatRates map[string]float64) (float6
|
||||
}
|
||||
|
||||
func (p *ForexPlugin) cmdReport(ctx MessageContext, args []string) error {
|
||||
if msg := fxValidateAnalysisArgs(args); msg != "" {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||
}
|
||||
if pair := fxParsePair(args, false); pair != nil {
|
||||
return p.cmdPairRateOrReport(ctx, pair, true)
|
||||
}
|
||||
@@ -404,6 +411,34 @@ func (p *ForexPlugin) checkAlerts(rates map[string]float64) {
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// fxValidateAnalysisArgs vets the currency tokens passed to the fiat-only
|
||||
// rate/report path. It returns a player-facing error message if any token is
|
||||
// unusable here, or "" when the args are fine (including empty args, which fall
|
||||
// back to the default tracked set). Crypto tokens get a dedicated nudge since
|
||||
// crypto only works on the conversion path; anything else is just invalid.
|
||||
func fxValidateAnalysisArgs(args []string) string {
|
||||
for _, a := range args {
|
||||
// Split pair syntax (BTC/USD) into its sides so each is checked.
|
||||
raw := strings.ToUpper(a)
|
||||
toks := []string{raw}
|
||||
for _, sep := range []string{"/", "|", "-"} {
|
||||
if strings.Contains(raw, sep) {
|
||||
toks = strings.SplitN(raw, sep, 2)
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, t := range toks {
|
||||
if fxIsCrypto(t) {
|
||||
return "Silly rabbit. Crypto is for kids! (In other words.. that's an invalid command — crypto only works for conversions like `!fx 100 USD to BTC`.)"
|
||||
}
|
||||
if !fxIsSupported(t) {
|
||||
return fmt.Sprintf("Don't know the currency `%s`. Try `!fx help`.", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func fxParseCurrencies(args []string) []string {
|
||||
var out []string
|
||||
for _, a := range args {
|
||||
|
||||
Reference in New Issue
Block a user