atagen

jelq

public

an http based roguelike rpg about jelqing

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