@@ -53,31 +53,49 @@ type Manager struct {
5353 interval time.Duration
5454 baseTTL time.Duration
5555
56+ questInterval time.Duration
57+ questDuration time.Duration
58+ now func () time.Time // injectable clock (quest deadlines); time.Now in prod
59+
5660 mu sync.Mutex
5761 online map [string ]player // network|nick -> online player
5862
63+ qmu sync.Mutex
64+ quest * quest // the active quest, nil when none
65+
5966 rmu sync.Mutex
6067 rng * rand.Rand
6168}
6269
6370// New builds a Manager. interval is the tick period; baseTTL is the level 0→1 time.
71+ // questInterval/questDuration tune the quest cadence (zero → sane defaults).
6472// resolve maps senders to canonical player keys (cross-network when linked).
65- func New (store state.Store , out engine.Sender , resolve Resolver , interval , baseTTL time.Duration , log * slog.Logger ) * Manager {
73+ func New (store state.Store , out engine.Sender , resolve Resolver , interval , baseTTL , questInterval , questDuration time.Duration , log * slog.Logger ) * Manager {
6674 if interval <= 0 {
6775 interval = 30 * time .Second
6876 }
6977 if baseTTL <= 0 {
7078 baseTTL = 10 * time .Minute
7179 }
80+ if questInterval <= 0 {
81+ questInterval = defaultQuestInterval
82+ }
83+ if questDuration <= 0 {
84+ questDuration = defaultQuestDuration
85+ }
7286 if resolve == nil {
7387 resolve = func (network , _ , nick string ) string { return strings .ToLower (network ) + "|" + strings .ToLower (nick ) }
7488 }
75- return & Manager {
89+ m := & Manager {
7690 store : store , out : out , resolve : resolve , log : log ,
7791 interval : interval , baseTTL : baseTTL ,
92+ questInterval : questInterval , questDuration : questDuration ,
93+ now : time .Now ,
7894 online : map [string ]player {},
7995 rng : rand .New (rand .NewSource (time .Now ().UnixNano ())),
8096 }
97+ m .loadQuest (context .Background ())
98+ return m
8199}
82100
83101// roll returns a value in [0,n). Guarded so battles/events can roll concurrently.
@@ -111,13 +129,15 @@ func (m *Manager) Handle(msg engine.Message) bool {
111129 m .command (msg , fields )
112130 return true
113131 }
114- // Talking is the cardinal sin — penalize online players.
132+ // Talking is the cardinal sin — penalize online players, and if they're on a
133+ // quest, talking blows the whole quest.
115134 if p , ok := m .onlinePlayer (msg .Network , msg .Nick ); ok {
116135 pen := int64 (len (msg .Text ))
117136 if pen > talkCapSec {
118137 pen = talkCapSec
119138 }
120139 _ , _ = m .store .HIncr (context .Background (), sheetKey (p .key ), "ttl" , pen )
140+ m .questViolation (context .Background (), p .key , p .nick , "spoke up" )
121141 }
122142 return false
123143}
@@ -302,34 +322,46 @@ func (m *Manager) penalizeOnline(network, nick string, secs int64) {
302322 }
303323}
304324
305- // OnPart / OnQuit / OnKick penalize then take the player offline.
325+ // OnPart / OnQuit / OnKick penalize, break any quest they were on, then take the
326+ // player offline.
306327func (m * Manager ) OnPart (ev event.Event ) {
307328 if ev .Kind == event .Part {
308329 m .penalizeOnline (ev .Network , ev .Nick , partPenalty )
330+ m .failQuestBy (ev .Network , ev .Nick , "abandoned the party" )
309331 m .OnLeave (ev )
310332 }
311333}
312334
313335func (m * Manager ) OnQuit (ev event.Event ) {
314336 if ev .Kind == event .Quit {
315337 m .penalizeOnline (ev .Network , ev .Nick , quitPenalty )
338+ m .failQuestBy (ev .Network , ev .Nick , "vanished from the realm" )
316339 m .OnLeave (ev )
317340 }
318341}
319342
320343func (m * Manager ) OnKick (ev event.Event ) {
321344 if ev .Kind == event .Kick {
322345 m .penalizeOnline (ev .Network , ev .Nick , kickPenalty )
346+ m .failQuestBy (ev .Network , ev .Nick , "was hurled from the channel" )
323347 m .OnLeave (ev )
324348 }
325349}
326350
327- // OnNick penalizes a nick change and follows the player to their new nick.
351+ // failQuestBy ruins the active quest if (network, nick) resolves to a quester.
352+ func (m * Manager ) failQuestBy (network , nick , reason string ) {
353+ key := m .resolve (network , "" , nick )
354+ m .questViolation (context .Background (), key , nick , reason )
355+ }
356+
357+ // OnNick penalizes a nick change, breaks any quest, and follows the player to
358+ // their new nick.
328359func (m * Manager ) OnNick (ev event.Event ) {
329360 if ev .Kind != event .Nick {
330361 return
331362 }
332363 m .penalizeOnline (ev .Network , ev .Nick , nickPenalty )
364+ m .failQuestBy (ev .Network , ev .Nick , "slipped away under a new name" )
333365 m .mu .Lock ()
334366 if p , ok := m .online [okey (ev .Network , ev .Nick )]; ok {
335367 delete (m .online , okey (ev .Network , ev .Nick ))
@@ -353,6 +385,7 @@ func (m *Manager) Tick() {
353385 step = 1
354386 }
355387 m .maybeEvent (context .Background ())
388+ m .questTick (context .Background ())
356389 for _ , p := range m .snapshot () {
357390 ctx := context .Background ()
358391 key := sheetKey (p .key )
0 commit comments