atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
3f9091557ec3a198324147b05397e1bf5c50247a
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/floors.rsRust237 lines8.3 KB
1
use axum::http::{StatusCode, header};
2
use serde_json::{Value, json};
3
4
use super::{
5
    Cause, Elder, Floor, FloorState, Item, Outcome, Run,
6
    balance::{
7
        EQ_BAND, FLOOR_ALLOWANCE, FLOOR_TURNS, FRICTION_FLOOR, KEY_IN_DRAWER, RULER_IN_DRAWER,
8
        STARTING_EQ, SUSPICION_LIMIT, ZIP,
9
    },
10
    round2,
11
};
12
use crate::problem::Problem;
13
14
impl Run {
15
    pub(super) fn next_floor(&mut self, paid: bool) {
16
        let Some(next) = self.floor.next() else {
17
            return;
18
        };
19
        let warm = EQ_BAND.contains(&self.eq);
20
        self.floor = next;
21
        self.here = FloorState {
22
            warm_arrival: warm,
23
            ..FloorState::default()
24
        };
25
        self.warmed_up = false;
26
        self.reset_eq(STARTING_EQ);
27
        self.suspicion = 0;
28
        self.shed(FRICTION_FLOOR);
29
        self.mum = None;
30
        if paid {
31
            self.cash += FLOOR_ALLOWANCE;
32
        }
33
        self.touch();
34
    }
35
36
    pub fn advance(&mut self) -> Result<Outcome, Problem> {
37
        match self.floor {
38
            Floor::Clinic => {
39
                return Err(Problem::new(
40
                    StatusCode::CONFLICT,
41
                    "no-further-floor",
42
                    "There is no further floor. There is only the diagnosis.",
43
                ));
44
            }
45
            Floor::Forum if self.elder != Elder::Beaten => {
46
                return Err(Problem::new(
47
                    StatusCode::CONFLICT,
48
                    "elder-unaddressed",
49
                    "The Elder has not been addressed.",
50
                )
51
                .with("elder", self.path("/elder")));
52
            }
53
            floor if floor.timed() && self.here.turns < FLOOR_TURNS => {
54
                return Err(Problem::new(
55
                    StatusCode::TOO_EARLY,
56
                    "floor-not-over",
57
                    format!(
58
                        "The floor is not over. {} turns remain.",
59
                        FLOOR_TURNS - self.here.turns
60
                    ),
61
                ));
62
            }
63
            _ => {}
64
        }
65
        if self.chance(
66
            self.decision(
67
                "zip",
68
                "As the floor ends and you put yourself away, does the zip catch?",
69
            ),
70
            ZIP,
71
        ) {
72
            return Err(self.end(Cause::Zip));
73
        }
74
        let left = self.floor.name();
75
        let reverted = self.here.temp_length;
76
        self.next_floor(true);
77
        let warm = if self.here.warm_arrival {
78
            ", and the warmth came with you"
79
        } else {
80
            ""
81
        };
82
        let mut body = json!({
83
            "detail": format!("You leave {left}. You have arrived at {}{warm}.", self.floor.name()),
84
            "note": self.floor.note(),
85
        });
86
        if reverted > 0.0 {
87
            body["reverted"] = json!(round2(reverted));
88
        }
89
        Ok(
90
            Outcome::json(StatusCode::SEE_OTHER, body)
91
                .header(header::LOCATION, self.path("/floor")),
92
        )
93
    }
94
95
    pub fn floor_view(&self) -> Value {
96
        let mut view = json!({
97
            "name": self.floor.name(),
98
            "note": self.floor.note(),
99
            "turns": self.here.turns,
100
            "turns_required": self.floor.timed().then_some(FLOOR_TURNS),
101
            "exit": { "href": self.path("/floor"), "method": "POST" },
102
        });
103
        match self.floor {
104
            Floor::ChangingRoom => {
105
                view["others"] = json!([
106
                    { "name": "Greg", "self_reported_length": 9.5 },
107
                    { "name": "Dmitri", "self_reported_length": 8.75 },
108
                    { "name": "a man in a towel", "self_reported_length": 11.0 },
109
                ]);
110
            }
111
            Floor::Hotel => view["portal"] = json!(self.path("/portal")),
112
            Floor::Forum => {
113
                view["thread"] = json!(super::FORUM_THREAD);
114
                view["elder"] = json!(self.path("/elder"));
115
            }
116
            Floor::Clinic => view["diagnosis"] = json!(self.path("/diagnosis")),
117
            _ => {}
118
        }
119
        if self.floor.shop_open() {
120
            view["shop"] = json!(self.path("/shop"));
121
        }
122
        view
123
    }
124
125
    fn found(&self, item: Item, detail: &str) -> Outcome {
126
        Outcome::json(StatusCode::FOUND, json!({ "detail": detail })).header(
127
            header::LOCATION,
128
            self.path(&format!("/inventory/{}", item.slug())),
129
        )
130
    }
131
132
    pub fn search_drawer(&mut self) -> Result<Outcome, Problem> {
133
        self.here.drawer_searches += 1;
134
        self.touch();
135
        let first = self.here.drawer_searches == 1;
136
        if self.items.cage {
137
            return Ok(self.rummage_for_key());
138
        }
139
        let ruler_here = self.floor == Floor::Bedroom
140
            && self.chance(
141
                self.decision("ruler", "Is there a ruler hiding in the drawer?"),
142
                RULER_IN_DRAWER,
143
            );
144
        match self.floor {
145
            Floor::Bedroom if first && !self.items.ruler && ruler_here => {
146
                self.items.grant(Item::Ruler);
147
                self.shame += 10;
148
                Ok(self.found(
149
                    Item::Ruler,
150
                    "You now know. This is the first honest measurement of the run.",
151
                ))
152
            }
153
            Floor::Bathroom if first => {
154
                self.items.grant(Item::DoorLock);
155
                Ok(self.found(Item::DoorLock, "A door lock. It works once."))
156
            }
157
            Floor::ChangingRoom => {
158
                self.shame += 3;
159
                Err(Problem::new(
160
                    StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
161
                    "not-your-locker",
162
                    "That locker belongs to Greg. The server cannot serve this request for legal reasons.",
163
                ))
164
            }
165
            Floor::Nans if first => {
166
                self.items.grant(Item::Cage);
167
                self.shame += 5;
168
                Ok(self.found(
169
                    Item::Cage,
170
                    "A small box. Inside was a cage. It is now on. The key is not in the box.",
171
                ))
172
            }
173
            Floor::Hotel if first => {
174
                self.items.grant(Item::DndSign);
175
                self.items.grant(Item::IcePack);
176
                self.items.grant(Item::IcePack);
177
                Ok(self.found(
178
                    Item::DndSign,
179
                    "A Do Not Disturb sign, and two ice packs from the machine down the hall.",
180
                ))
181
            }
182
            Floor::Forum if first => {
183
                self.items.grant(Item::Spreadsheet);
184
                Ok(self.found(
185
                    Item::Spreadsheet,
186
                    "Your own spreadsheet. Every line is flat.",
187
                ))
188
            }
189
            Floor::Clinic => Err(Problem::new(
190
                StatusCode::FORBIDDEN,
191
                "doctors-drawer",
192
                "That is the doctor's drawer.",
193
            )),
194
            _ => Ok(Outcome::empty(StatusCode::NO_CONTENT)),
195
        }
196
    }
197
198
    pub fn accept_terms(&mut self) -> Result<Outcome, Problem> {
199
        if self.floor != Floor::Hotel {
200
            return Err(Problem::new(
201
                StatusCode::NOT_FOUND,
202
                "no-portal",
203
                "There is no captive portal here. There is only the regular kind of captivity.",
204
            ));
205
        }
206
        self.here.wifi_accepted = true;
207
        self.touch();
208
        Ok(Outcome::json(
209
            StatusCode::SEE_OTHER,
210
            json!({ "detail": "Connected. The hotel does not want to know." }),
211
        )
212
        .header(header::LOCATION, self.path("")))
213
    }
214
215
    /// Searching while caged is noisy, and the key only turns up some of the time.
216
    fn rummage_for_key(&mut self) -> Outcome {
217
        let was_below = self.suspicion < SUSPICION_LIMIT;
218
        self.suspicion = (self.suspicion + 1).min(SUSPICION_LIMIT);
219
        self.maybe_summon_mum(was_below);
220
        if !self.chance(
221
            self.decision("key", "Does the drawer finally give up the cage key?"),
222
            KEY_IN_DRAWER,
223
        ) {
224
            return Outcome::empty(StatusCode::NO_CONTENT);
225
        }
226
        self.items.grant(Item::CageKey);
227
        Outcome::json(
228
            StatusCode::OK,
229
            json!({ "detail": "The key was in the drawer. The cage is off." }),
230
        )
231
    }
232
233
    /// Whether searching the drawer on this floor would turn anything up.
234
    pub fn drawer_worth_searching(&self) -> bool {
235
        self.here.drawer_searches == 0 || self.items.cage
236
    }
237
}