aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_quit.go
blob: 50f1ec589c7129cd2f6f4847ddfe129963466e75 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package game

import (
	"strings"

	"thehouseoficarus/internal/engine"
	"thehouseoficarus/internal/net"
)

func (g *Game) executeQuit(sess *net.Session, args []string, rawInput string) {
	if len(args) > 0 && strings.EqualFold(args[0], "now") {
		g.doQuitNow(sess)
		return
	}
	g.doQuit(sess)
}

func (g *Game) doQuit(sess *net.Session) {
	p := sess.Player

	// Combat blocks quitting outright; check it before departing any casino
	// game so a cancelled quit never costs the player their round.
	if g.Combat.Get(p.Name) != nil {
		sess.WriteLine("You can't rest during combat!")
		return
	}

	// Depart casino games the way "stop" does: a table with a wager committed
	// to the round in flight refuses and the quit is aborted, so deliberate
	// exits never forfeit a stake or reveal cards mid-hand. Forfeit-on-leave
	// stays reserved for genuine disconnects and "quit now".
	if !g.leaveCasinoGames(p) {
		// A committed wager blocks the departure; whether it is actually lost
		// depends on whether the game has dealt/started a round for it. A
		// stake the game has not dealt for yet is refunded on disconnect
		// (baccarat always, blackjack while the betting window is open), and
		// a dealt hand or a spin in progress is forfeited. The refusal
		// therefore states both outcomes instead of promising a forfeit that
		// an undealt stake does not suffer.
		sess.WriteLine(g.colorize(sess, "warning", "Type \"quit now\" to log out anyway — a bet still awaiting the deal is refunded; a hand or spin in progress is forfeited."))
		return
	}

	g.beginQuitCountdown(sess)
}

// doQuitNow force-quits past a table's mid-round refusal. The logout
// countdown starts immediately, but the seat and wager are only forfeited
// on the countdown's final tick, immediately before the disconnect — until
// then any active or free command (or "stop") cancels the countdown and
// leaves the wager intact.
func (g *Game) doQuitNow(sess *net.Session) {
	p := sess.Player
	if g.Combat.Get(p.Name) != nil {
		sess.WriteLine("You can't rest during combat!")
		return
	}
	if g.inCasinoGame(p.Name) {
		sess.WriteLine(g.colorize(sess, "warning", "WARNING: Logging out mid-game will forfeit your bet! Type \"stop\" to cancel."))
	}
	g.beginQuitCountdown(sess)
}

// beginQuitCountdown runs the shared logout sequence: stop any current
// action, then count ten ticks down to the final tick that severs the
// session. Casino participation (a "quit now" mid-game) is resolved on
// that final tick via MarkDisconnected, so the countdown stays cancellable
// the whole way down.
func (g *Game) beginQuitCountdown(sess *net.Session) {
	p := sess.Player
	g.cancelAction(p)
	g.cancelRest(p.Name)

	g.broadcastAction(sess, "%s sits down to rest.", p.Name)

	ticksLeft := 10
	var id uint64
	id = g.SubscribeTicks(engine.ToTicks(1), func() bool {
		// The engine snapshots its subscribers before firing them, so this
		// callback can still run once after cancelRest unsubscribes it. Bail
		// out unless this is still the registered countdown, so a quit
		// cancelled on the final tick never force-disconnects the player (and
		// never forfeits a committed casino wager).
		if g.restTimers[p.Name] != id {
			return false
		}
		switch ticksLeft {
		case 10:
			sess.WriteLine("You sit down to rest...")
		case 6:
			sess.WriteLine("You catch your breath...")
		case 3:
			sess.WriteLine("You close your eyes...")
		case 0:
			// Release combat, cover, actions and hacking state before the
			// disconnect. This path used to leave the character occupying a
			// safespot cover after logging out.
			g.releaseCharacterRuntime(sess, p)
			g.restoreGodPlayer(p)
			// Forfeit any committed casino wager and free the seat only
			// now — immediately before the disconnect. Settling games only
			// pay seated bettors, so this forfeits a mid-round stake.
			if g.Casino != nil {
				g.emitCasinoEvents(g.Casino.MarkDisconnected(p.Name))
			}
			g.saveCharacter(p)
			// Drop the registry entry and any queued commands: leftover queue
			// entries retain this session and would replay on a later login as
			// the same character.
			g.detachSession(p.Name)
			if g.Hub != nil {
				g.Hub.LeaveRoom(sess)
			}
			if g.Hub != nil {
				g.Hub.SetSessionPlayer(sess, nil)
			} else {
				sess.Player = nil
			}
			sess.SetState(net.StateMenu)
			g.showMenu(sess)
			return false
		}
		ticksLeft--
		return true
	})
	g.restTimers[p.Name] = id
}

func (g *Game) cancelRest(playerName string) {
	if id, ok := g.restTimers[playerName]; ok {
		g.Ticks.Unsubscribe(id)
		delete(g.restTimers, playerName)
	}
}