atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
a6caa2f9978665374ee51870e78151e4324328d0
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/floors.rsRust285 lines10.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
        DRAWER_JAMS, EQ_BAND, FLOOR_ALLOWANCE, FLOOR_TURNS, FRICTION_FLOOR, GREGS_LOCKER,
8
        KEY_IN_DRAWER, RULER_IN_DRAWER, 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.touch();
134
        self.end_kitchen_visit();
135
        if self.items.cage {
136
            return Ok(self.rummage_for_key());
137
        }
138
        let first = !self.here.drawer_searched;
139
        self.here.drawer_searched = true;
140
        let ruler_here = self.floor == Floor::Bedroom
141
            && self.chance(
142
                self.decision("ruler", "Is there a ruler hiding in the drawer?"),
143
                RULER_IN_DRAWER,
144
            );
145
        let vaseline_here = self.floor == Floor::Bathroom
146
            && first
147
            && self.chance(
148
                self.decision(
149
                    "bathroom-vaseline",
150
                    "Is it the vaseline behind the door lock, rather than the lotion?",
151
                ),
152
                2,
153
            );
154
        let locker_open = self.floor == Floor::ChangingRoom
155
            && first
156
            && self.chance(
157
                self.decision("gregs-locker", "Has Greg left his locker open?"),
158
                GREGS_LOCKER,
159
            );
160
        match self.floor {
161
            Floor::Bedroom if first && !self.items.ruler && ruler_here => {
162
                self.items.grant(Item::Ruler);
163
                self.shame += 10;
164
                Ok(self.found(
165
                    Item::Ruler,
166
                    "You now know. This is the first honest measurement of the run.",
167
                ))
168
            }
169
            Floor::Bathroom if first => {
170
                self.items.grant(Item::DoorLock);
171
                let detail = if vaseline_here {
172
                    self.items.grant(Item::Vaseline);
173
                    "A door lock, which works once, and a tub of vaseline behind it."
174
                } else {
175
                    self.items.grant(Item::Lotion);
176
                    "A door lock, which works once, and a bottle of lotion behind it."
177
                };
178
                Ok(self.found(Item::DoorLock, detail))
179
            }
180
            Floor::ChangingRoom if locker_open => {
181
                self.items.grant(Item::BabyOil);
182
                Ok(self.found(
183
                    Item::BabyOil,
184
                    "Greg has left his locker open. The baby oil is yours now, legally speaking.",
185
                ))
186
            }
187
            Floor::ChangingRoom => {
188
                self.shame += 3;
189
                Err(Problem::new(
190
                    StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,
191
                    "not-your-locker",
192
                    "That locker belongs to Greg. The server cannot serve this request for legal reasons.",
193
                ))
194
            }
195
            Floor::Nans if first => {
196
                self.items.grant(Item::Cage);
197
                self.shame += 5;
198
                Ok(self.found(
199
                    Item::Cage,
200
                    "A small box. Inside was a cage. It is now on. The key is not in the box.",
201
                ))
202
            }
203
            Floor::Hotel if first => {
204
                self.items.grant(Item::DndSign);
205
                self.items.grant(Item::IcePack);
206
                self.items.grant(Item::IcePack);
207
                self.items.grant(Item::Lotion);
208
                Ok(self.found(
209
                    Item::DndSign,
210
                    "A Do Not Disturb sign, two ice packs from the machine down the hall, and a complimentary lotion.",
211
                ))
212
            }
213
            Floor::Forum if first => {
214
                self.items.grant(Item::Spreadsheet);
215
                Ok(self.found(
216
                    Item::Spreadsheet,
217
                    "Your own spreadsheet. Every line is flat.",
218
                ))
219
            }
220
            Floor::Clinic => Err(Problem::new(
221
                StatusCode::FORBIDDEN,
222
                "doctors-drawer",
223
                "That is the doctor's drawer.",
224
            )),
225
            _ => Ok(Outcome::empty(StatusCode::NO_CONTENT)),
226
        }
227
    }
228
229
    pub fn accept_terms(&mut self) -> Result<Outcome, Problem> {
230
        if self.floor != Floor::Hotel {
231
            return Err(Problem::new(
232
                StatusCode::NOT_FOUND,
233
                "no-portal",
234
                "There is no captive portal here. There is only the regular kind of captivity.",
235
            ));
236
        }
237
        self.here.wifi_accepted = true;
238
        self.touch();
239
        Ok(Outcome::json(
240
            StatusCode::SEE_OTHER,
241
            json!({ "detail": "Connected. The hotel does not want to know." }),
242
        )
243
        .header(header::LOCATION, self.path("")))
244
    }
245
246
    /// Searching while caged is noisy, costs the floor turn, and the key only turns up some of
247
    /// the time. A jammed drawer takes a whole turn to work loose.
248
    fn rummage_for_key(&mut self) -> Outcome {
249
        self.turn += 1;
250
        self.here.turns += 1;
251
        self.realise();
252
        let was_below = self.suspicion < SUSPICION_LIMIT;
253
        self.suspicion = (self.suspicion + 1).min(SUSPICION_LIMIT);
254
        self.maybe_summon_mum(was_below);
255
        if self.here.drawer_jammed {
256
            self.here.drawer_jammed = false;
257
            self.notices
258
                .push("The drawer comes loose. That took a while.");
259
            return Outcome::empty(StatusCode::NO_CONTENT);
260
        }
261
        if !self.chance(
262
            self.decision("key", "Does the drawer finally give up the cage key?"),
263
            KEY_IN_DRAWER,
264
        ) {
265
            if self.chance(
266
                self.decision("drawer-jams", "Has the drawer jammed on its runners?"),
267
                DRAWER_JAMS,
268
            ) {
269
                self.here.drawer_jammed = true;
270
                self.notices.push("The drawer has jammed.");
271
            }
272
            return Outcome::empty(StatusCode::NO_CONTENT);
273
        }
274
        self.items.grant(Item::CageKey);
275
        Outcome::json(
276
            StatusCode::OK,
277
            json!({ "detail": "The key was in the drawer. The cage is off." }),
278
        )
279
    }
280
281
    /// Whether searching the drawer on this floor would turn anything up.
282
    pub fn drawer_worth_searching(&self) -> bool {
283
        !self.here.drawer_searched || self.items.cage
284
    }
285
}