atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
d2e314cf03d59c70462af9b534335ce881b718eb
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/jelq.rsRust677 lines24.0 KB
1
use std::{
2
    fmt::Write as _,
3
    time::{Duration, Instant, SystemTime},
4
};
5
6
use axum::http::{StatusCode, header};
7
use rand::Rng;
8
use serde_json::json;
9
10
use super::{
11
    CLINIC_ALLOW, FORUM_THREAD, Floor, Grip, Lube, Outcome, REFRACTORY, Run, SERVER_TIMING,
12
    SUSPICION_LIMIT, Technique, catalog::LubeRule, catalog::Profile, not_extended, round3,
13
};
14
use crate::{game::LUBRICANTS, problem::Problem};
15
16
const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
17
const PRIAPISM_TURNS: u8 = 5;
18
const PRIAPISM_EQ: i32 = 75;
19
const DEGLOVED: &str =
20
    "The outer layer has been removed in one piece. It will not be returning. The run has ended.";
21
22
impl Run {
23
    pub fn jelq(
24
        &mut self,
25
        techniques: &[Technique],
26
        intensity: i32,
27
        lube: Option<Lube>,
28
        now: Instant,
29
    ) -> Result<Outcome, Problem> {
30
        self.check_refractory(now)?;
31
        if self.branch_pending {
32
            return Ok(self.branch_offer());
33
        }
34
        match self.floor {
35
            Floor::Forum => {
36
                return Ok(Outcome::json(
37
                    StatusCode::SEE_OTHER,
38
                    json!({ "detail": "Refer to the forum." }),
39
                )
40
                .header(header::LOCATION, FORUM_THREAD));
41
            }
42
            Floor::Clinic => {
43
                return Err(Problem::new(
44
                    StatusCode::METHOD_NOT_ALLOWED,
45
                    "not-in-the-clinic",
46
                    "JELQ is not performed in the clinic.",
47
                )
48
                .header(header::ALLOW, CLINIC_ALLOW));
49
            }
50
            _ => {}
51
        }
52
        if self.items.cage {
53
            return Err(Problem::new(
54
                StatusCode::LOCKED,
55
                "caged",
56
                "The cage is locked. The key is not in the box.",
57
            ));
58
        }
59
        match *techniques {
60
            [technique] => self.single(technique, intensity, lube, now),
61
            [a, b] => self.combo(a, b, intensity, lube, now),
62
            _ => Err(Problem::new(
63
                StatusCode::BAD_REQUEST,
64
                "too-many-techniques",
65
                "Two techniques at most. The server has limits even if you do not.",
66
            )),
67
        }
68
    }
69
70
    fn single(
71
        &mut self,
72
        technique: Technique,
73
        intensity: i32,
74
        lube: Option<Lube>,
75
        now: Instant,
76
    ) -> Result<Outcome, Problem> {
77
        match technique {
78
            Technique::Warmup => return self.warm_up(),
79
            Technique::Ice => return self.ice(),
80
            Technique::Supplement => return self.supplement(),
81
            Technique::Edge => {}
82
            _ => self.edges = 0,
83
        }
84
        self.prepare(technique, lube)?;
85
        self.check_storage()?;
86
        self.floor_enemy()?;
87
        match technique {
88
            Technique::Edge => return self.edge(now),
89
            Technique::Pump => return self.pump(now),
90
            _ => {}
91
        }
92
        let profile = technique.profile().expect("training technique");
93
        self.commit(intensity, lube, now, 1);
94
        if technique == Technique::Dry && intensity >= 8 && self.rng.random_ratio(1, 60) {
95
            self.end("degloved", DEGLOVED);
96
            return Err(Problem::new(StatusCode::GONE, "degloved", DEGLOVED));
97
        }
98
        if let Some(interrupted) = self.hazards(intensity)? {
99
            return Ok(interrupted);
100
        }
101
        let result = self.roll(technique, &profile, intensity, lube);
102
        Ok(self.gossip(result?))
103
    }
104
105
    fn combo(
106
        &mut self,
107
        a: Technique,
108
        b: Technique,
109
        intensity: i32,
110
        lube: Option<Lube>,
111
        now: Instant,
112
    ) -> Result<Outcome, Problem> {
113
        let (Some(first), Some(second)) = (a.profile(), b.profile()) else {
114
            return Err(Problem::new(
115
                StatusCode::BAD_REQUEST,
116
                "combo-invalid",
117
                "Only training techniques can be combined.",
118
            ));
119
        };
120
        if a == b {
121
            return Err(Problem::new(
122
                StatusCode::BAD_REQUEST,
123
                "combo-invalid",
124
                "A technique cannot be combined with itself. The server has checked.",
125
            ));
126
        }
127
        self.edges = 0;
128
        self.prepare(a, lube)?;
129
        self.prepare(b, lube)?;
130
        self.check_storage()?;
131
        self.floor_enemy()?;
132
        self.commit(intensity, lube, now, 2);
133
        if let Some(interrupted) = self.hazards(intensity)? {
134
            return Ok(interrupted);
135
        }
136
137
        let mut statuses = Vec::new();
138
        for (technique, profile) in [(a, first), (b, second)] {
139
            let result = self.roll(technique, &profile, intensity, lube);
140
            if self.ended.is_some() {
141
                return result;
142
            }
143
            let status = match &result {
144
                Ok(outcome) => outcome.status(),
145
                Err(problem) => problem.status(),
146
            };
147
            statuses.push((technique, status));
148
        }
149
150
        let mut xml = String::from(
151
            "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n",
152
        );
153
        for (technique, status) in statuses {
154
            let _ = write!(
155
                xml,
156
                "  <D:response>\n    <D:href>/runs/{}?technique={}</D:href>\n    <D:status>HTTP/1.1 {} {}</D:status>\n  </D:response>\n",
157
                self.id,
158
                technique.name(),
159
                status.as_u16(),
160
                status.canonical_reason().unwrap_or_default(),
161
            );
162
        }
163
        xml.push_str(
164
            "  <D:responsedescription>Each technique has been reported separately.</D:responsedescription>\n</D:multistatus>\n",
165
        );
166
        Ok(Outcome::raw(
167
            StatusCode::MULTI_STATUS,
168
            "application/xml; charset=utf-8",
169
            xml,
170
        ))
171
    }
172
173
    fn prepare(&self, technique: Technique, lube: Option<Lube>) -> Result<(), Problem> {
174
        if technique == Technique::Uli && !self.forum_read {
175
            return Err(Problem::new(
176
                StatusCode::FORBIDDEN,
177
                "technique-locked",
178
                "This technique is discussed on the forum. You have not been to the forum.",
179
            ));
180
        }
181
        if !self.warmed_up {
182
            return Err(Problem::new(
183
                StatusCode::PRECONDITION_FAILED,
184
                "cold-start",
185
                "Warm-up was skipped. The server has noted this.",
186
            )
187
            .with("technique", Technique::Warmup.name()));
188
        }
189
        if technique == Technique::Pump && !self.items.pump_pro {
190
            return Err(Problem::new(
191
                StatusCode::FAILED_DEPENDENCY,
192
                "no-pump",
193
                "The pump technique depends on a pump.",
194
            ));
195
        }
196
        let Some(profile) = technique.profile() else {
197
            return Ok(());
198
        };
199
        match (profile.lube, lube) {
200
            (LubeRule::Required, None) => {
201
                return Err(Problem::new(
202
                    StatusCode::UNSUPPORTED_MEDIA_TYPE,
203
                    "lubricant-required",
204
                    format!(
205
                        "The {} technique requires a lubricant. None was declared.",
206
                        technique.name()
207
                    ),
208
                )
209
                .with("lubricants", json!(LUBRICANTS)));
210
            }
211
            (LubeRule::Forbidden, Some(_)) => {
212
                return Err(Problem::new(
213
                    StatusCode::UNSUPPORTED_MEDIA_TYPE,
214
                    "lubricant-declared",
215
                    "The dry technique was requested. A lubricant was declared.",
216
                ));
217
            }
218
            _ => {}
219
        }
220
        if let Some((min, max)) = profile.foreskin {
221
            self.check_foreskin(min, max)?;
222
        }
223
        Ok(())
224
    }
225
226
    fn check_foreskin(&self, min: f64, max: f64) -> Result<(), Problem> {
227
        let Some(f) = self.foreskin else {
228
            if min <= 0.0 {
229
                return Ok(());
230
            }
231
            return Err(Problem::new(
232
                StatusCode::GONE,
233
                "foreskin-gone",
234
                "This technique requires a foreskin. The foreskin is gone.",
235
            )
236
            .header(
237
                header::CONTENT_LOCATION,
238
                format!("/runs/{}/foreskin", self.id),
239
            ));
240
        };
241
        if f < min {
242
            return Err(Problem::new(
243
                StatusCode::LENGTH_REQUIRED,
244
                "foreskin-insufficient",
245
                format!(
246
                    "The selected technique requires {min:.1} units of foreskin. {f:.2} units are available. The server has noted the shortfall."
247
                ),
248
            )
249
            .with("required", min)
250
            .with("available", super::round2(f)));
251
        }
252
        if f > max {
253
            return Err(Problem::new(
254
                StatusCode::PAYLOAD_TOO_LARGE,
255
                "foreskin-excessive",
256
                format!(
257
                    "The selected technique tolerates at most {max:.1} units of foreskin. {f:.2} units are present."
258
                ),
259
            )
260
            .with("maximum", max)
261
            .with("present", super::round2(f)));
262
        }
263
        Ok(())
264
    }
265
266
    fn check_storage(&self) -> Result<(), Problem> {
267
        if self.items.extender && self.girth > 5.0 {
268
            return Err(Problem::new(
269
                StatusCode::INSUFFICIENT_STORAGE,
270
                "jeans-full",
271
                "The jeans are full. The extender is occupying the remaining storage.",
272
            )
273
            .with("extender", format!("/runs/{}/inventory/extender", self.id)));
274
        }
275
        Ok(())
276
    }
277
278
    fn commit(&mut self, intensity: i32, lube: Option<Lube>, now: Instant, cost: i32) {
279
        self.touch();
280
        self.turn += 1;
281
        self.floor_turns += 1;
282
        self.realize();
283
        if self.items.extender {
284
            self.length += 0.004;
285
        }
286
        self.stamina -= cost + i32::from(lube == Some(Lube::Lotion));
287
        if self.stamina <= 0 {
288
            self.stamina = 0;
289
            self.refractory_until = Some(now + *REFRACTORY);
290
        }
291
        let chill = if self.floor == Floor::LockerRoom {
292
            8
293
        } else {
294
            0
295
        };
296
        let wobble = self.rng.random_range(-3..=3);
297
        self.eq = (self.eq + intensity * 2 - 6 - chill + wobble).clamp(0, 100);
298
299
        let mut suspicion = intensity / 5 + i32::from(lube == Some(Lube::Vaseline));
300
        if matches!(self.floor, Floor::LockerRoom | Floor::Grandmas) {
301
            suspicion *= 2;
302
        }
303
        if self.floor == Floor::Grandmas && lube == Some(Lube::CoconutOil) {
304
            suspicion += 2;
305
        }
306
        let was_below = self.suspicion < SUSPICION_LIMIT;
307
        self.suspicion =
308
            (i32::from(self.suspicion) + suspicion).min(i32::from(SUSPICION_LIMIT)) as u8;
309
        if self.floor.has_mom() && !self.mom_present {
310
            let summoned = was_below && self.suspicion == SUSPICION_LIMIT;
311
            if summoned || self.rng.random_ratio(u32::from(self.suspicion) + 1, 16) {
312
                self.mom_present = true;
313
                self.mom_strikes = 0;
314
                self.mom_errand = self.errand(summoned);
315
                self.nonce = self.fresh_nonce();
316
            }
317
        }
318
319
        if let Some(observer) = self.floor.observer()
320
            && self.rng.random_ratio(1, 6)
321
        {
322
            self.observed = Some(observer);
323
            self.shame += 1;
324
            if observer == "cat" && self.rng.random_ratio(1, 5) {
325
                self.towel_stolen = true;
326
            }
327
        }
328
        if intensity >= 9 && !self.pulled_muscle && self.rng.random_ratio(1, 12) {
329
            self.pulled_muscle = true;
330
            self.stamina = self.stamina.min(self.max_stamina());
331
        }
332
        if self.items.ring && self.eq > 85 && self.rng.random_ratio(1, 10) {
333
            self.priapism_turns = PRIAPISM_TURNS;
334
        }
335
        if self.priapism_turns > 0 {
336
            self.priapism_turns -= 1;
337
            self.eq = self.eq.max(PRIAPISM_EQ);
338
        }
339
        if self.turn == super::BRANCH_TURN {
340
            self.branch_pending = true;
341
        }
342
    }
343
344
    fn hazards(&mut self, intensity: i32) -> Result<Option<Outcome>, Problem> {
345
        let odds = match self.eq {
346
            ..=70 => 250,
347
            71..=85 => 25,
348
            _ => 10,
349
        };
350
        if self.rng.random_ratio(1, odds) {
351
            return Err(self.rupture());
352
        }
353
        let windowed = matches!(self.floor, Floor::Bedroom | Floor::Grandmas | Floor::Hotel);
354
        if windowed && !self.items.blinds && self.rng.random_ratio(1, 60) {
355
            self.shame += 30;
356
            self.filmed_at.get_or_insert_with(SystemTime::now);
357
            return Ok(Some(
358
                Outcome::json(
359
                    StatusCode::CREATED,
360
                    json!({ "detail": "A neighbour was at the window. A recording has been created. It has been shared." }),
361
                )
362
                .header(header::LOCATION, format!("/runs/{}/footage", self.id)),
363
            ));
364
        }
365
        if self.floor == Floor::LockerRoom && self.rng.random_ratio(1, 10) {
366
            self.shame += 1;
367
            return Err(Problem::new(
368
                StatusCode::USE_PROXY,
369
                "friend",
370
                "A friend has offered to help. Use Proxy is deprecated, and the server has declined on your behalf.",
371
            ));
372
        }
373
        if self.peyronie && self.rng.random_ratio(1, 3) {
374
            return Err(Problem::new(
375
                StatusCode::MISDIRECTED_REQUEST,
376
                "misdirected",
377
                "The technique landed slightly to the left. It was meant for a different server.",
378
            ));
379
        }
380
        if intensity <= 2 && self.eq < 50 {
381
            self.eq = (self.eq - 10).max(0);
382
            return Err(Problem::new(
383
                StatusCode::REQUEST_TIMEOUT,
384
                "lost-it",
385
                "The request took too long. It was lost.",
386
            )
387
            .header(header::CONNECTION, "close"));
388
        }
389
        if self.eq > 95 {
390
            if self.rng.random_ratio(1, 3) {
391
                return Err(self.rupture());
392
            }
393
            self.shame += 5;
394
            self.next_floor(false);
395
            return Err(Problem::new(
396
                StatusCode::TOO_EARLY,
397
                "premature",
398
                "The floor has ended early. Everyone involved has agreed not to discuss it.",
399
            )
400
            .with("floor", self.floor.number()));
401
        }
402
        if self.eq < 30 {
403
            return Err(Problem::new(
404
                StatusCode::NOT_FOUND,
405
                "cold",
406
                "The resource exists. It is cold.",
407
            )
408
            .with("eq", self.eq));
409
        }
410
        Ok(None)
411
    }
412
413
    fn roll(
414
        &mut self,
415
        technique: Technique,
416
        profile: &Profile,
417
        intensity: i32,
418
        lube: Option<Lube>,
419
    ) -> Result<Outcome, Problem> {
420
        let natural: i32 = self.rng.random_range(1..=20);
421
        let band = if (50..=70).contains(&self.eq) { 2 } else { 0 };
422
        let grip = match (self.grip, technique) {
423
            (Grip::Underhand, Technique::Stretch) => 2,
424
            (Grip::Underhand, Technique::Wet) => -1,
425
            _ => 0,
426
        };
427
        let modifier = lube.map_or(0, Lube::modifier) + band + grip;
428
        let dc = profile.dc + intensity / 3;
429
        let total = natural + modifier;
430
        let timing = format!("roll;desc=\"d20{modifier:+} vs {dc}\";dur={natural}");
431
        let timed = |p: Problem| p.header(SERVER_TIMING, &timing);
432
433
        let scale = f64::from(intensity);
434
        let (mut length, mut girth) = (profile.length * scale, profile.girth * scale);
435
        if self.teapot {
436
            length *= 0.5;
437
            girth *= 1.5;
438
        }
439
        if self.items.ring {
440
            girth *= 2.0;
441
        }
442
        let rupture_eq = if self.items.ring { 80 } else { 90 };
443
444
        if natural == 1 {
445
            let touches_foreskin = profile.foreskin.is_some_and(|(min, _)| min > 0.0);
446
            if touches_foreskin && self.foreskin.is_some() {
447
                self.foreskin = None;
448
                self.shame += 25;
449
                return Err(timed(
450
                    Problem::new(
451
                        StatusCode::GONE,
452
                        "foreskin-lost",
453
                        "The technique has removed the foreskin. It will not be returning.",
454
                    )
455
                    .header(
456
                        header::CONTENT_LOCATION,
457
                        format!("/runs/{}/foreskin", self.id),
458
                    ),
459
                ));
460
            }
461
            if self.eq > 70 {
462
                return Err(timed(self.rupture()));
463
            }
464
            if technique == Technique::Stretch && !self.teapot {
465
                self.teapot = true;
466
                self.items.teapot_spout = true;
467
                self.shame += 5;
468
                return Err(timed(Problem::new(
469
                    StatusCode::IM_A_TEAPOT,
470
                    "teapot",
471
                    "The subject is now short and stout. Length gains are halved for the rest of the floor.",
472
                )));
473
            }
474
            if technique == Technique::Uli && !self.peyronie {
475
                self.peyronie = true;
476
                self.shame += 5;
477
                return Err(timed(Problem::new(
478
                    StatusCode::MISDIRECTED_REQUEST,
479
                    "curvature",
480
                    "A curvature has developed. Techniques will land slightly to the left until a professional is consulted.",
481
                )));
482
            }
483
            return Err(timed(not_extended()));
484
        }
485
        if natural == 2 && self.eq > 70 {
486
            return Err(timed(self.rupture()));
487
        }
488
        if self.eq > rupture_eq && total < dc {
489
            return Err(timed(self.rupture()));
490
        }
491
        if natural == 20 {
492
            self.gain(length * 2.0, girth * 2.0, profile.foreskin_growth);
493
            return Ok(Outcome::json(
494
                StatusCode::CREATED,
495
                json!({
496
                    "detail": "A permanent gain has been created.",
497
                    "length": round3(length * 2.0),
498
                    "girth": round3(girth * 2.0),
499
                }),
500
            )
501
            .header(header::LOCATION, format!("/runs/{}/length", self.id))
502
            .header(SERVER_TIMING, timing));
503
        }
504
        if total < dc - 3 {
505
            return Err(timed(not_extended()));
506
        }
507
        if total < dc {
508
            return Ok(Outcome::empty(StatusCode::NO_CONTENT).header(SERVER_TIMING, timing));
509
        }
510
511
        let outcome = if self.eq < 50 {
512
            self.gain(length / 2.0, girth / 2.0, profile.foreskin_growth);
513
            Outcome::json(
514
                StatusCode::PARTIAL_CONTENT,
515
                json!({
516
                    "detail": "Partial gains were applied. EQ was below the band.",
517
                    "length": round3(length / 2.0),
518
                    "girth": round3(girth / 2.0),
519
                }),
520
            )
521
        } else if total >= dc + 5 {
522
            self.gain(length, girth, profile.foreskin_growth);
523
            Outcome::json(
524
                StatusCode::OK,
525
                json!({
526
                    "detail": "Gains were applied.",
527
                    "length": round3(length),
528
                    "girth": round3(girth),
529
                }),
530
            )
531
        } else {
532
            let eta = self.rng.random_range(1..=4);
533
            self.pending.push(super::Pending {
534
                due: self.turn + eta,
535
                length,
536
                girth,
537
            });
538
            self.gain(0.0, 0.0, profile.foreskin_growth);
539
            Outcome::json(
540
                StatusCode::ACCEPTED,
541
                json!({
542
                    "detail": "Gains have been queued. They may arrive.",
543
                    "eta_turns": eta,
544
                }),
545
            )
546
        };
547
        Ok(outcome.header(SERVER_TIMING, timing))
548
    }
549
550
    fn gossip(&mut self, outcome: Outcome) -> Outcome {
551
        let reportable = matches!(outcome.status.as_u16(), 200 | 202 | 206);
552
        if !self.roommate_told || !reportable {
553
            return outcome;
554
        }
555
        self.roommate_told = false;
556
        let mut outcome = outcome;
557
        outcome.status = StatusCode::ALREADY_REPORTED;
558
        if let super::Payload::Json(body) = &mut outcome.payload {
559
            body["detail"] = json!("The roommate has already told everyone.");
560
        }
561
        outcome
562
    }
563
564
    fn warm_up(&mut self) -> Result<Outcome, Problem> {
565
        if self.floor == Floor::Hotel && self.rng.random_ratio(1, 3) {
566
            return Err(Problem::new(
567
                StatusCode::SERVICE_UNAVAILABLE,
568
                "service-unavailable",
569
                "Service is unavailable. The server estimates next week. The server is being pessimistic.",
570
            )
571
            .header(
572
                header::RETRY_AFTER,
573
                httpdate::fmt_http_date(SystemTime::now() + WEEK),
574
            ));
575
        }
576
        let (boost, detail) = if self.floor == Floor::Bathroom {
577
            (
578
                35,
579
                "Warmed up. The shower is hot. The roommate's will not be.",
580
            )
581
        } else if self.towel_stolen {
582
            (10, "Warmed up, poorly. The cat has the towel.")
583
        } else {
584
            (25, "Warmed up. The towel has cooled.")
585
        };
586
        self.touch();
587
        self.warmed_up = true;
588
        self.eq = (self.eq + boost).min(100);
589
        Ok(Outcome::json(StatusCode::OK, json!({ "detail": detail })))
590
    }
591
592
    fn ice(&mut self) -> Result<Outcome, Problem> {
593
        if self.items.ice_packs == 0 {
594
            return Err(Problem::new(
595
                StatusCode::FAILED_DEPENDENCY,
596
                "no-ice",
597
                "The ice technique depends on an ice pack. There are none.",
598
            ));
599
        }
600
        self.touch();
601
        self.items.ice_packs -= 1;
602
        if self.priapism_turns > 0 {
603
            return Ok(Outcome::json(
604
                StatusCode::OK,
605
                json!({ "detail": "The ice has no effect. The condition persists. The ice pack is gone." }),
606
            ));
607
        }
608
        self.eq = (self.eq - 40).max(0);
609
        Ok(Outcome::json(
610
            StatusCode::OK,
611
            json!({ "detail": "EQ has been reduced. The ice pack is gone." }),
612
        ))
613
    }
614
615
    fn supplement(&mut self) -> Result<Outcome, Problem> {
616
        if self.items.supplements == 0 {
617
            return Err(Problem::new(
618
                StatusCode::FAILED_DEPENDENCY,
619
                "no-supplement",
620
                "The supplement technique depends on a supplement.",
621
            ));
622
        }
623
        self.items.supplements -= 1;
624
        self.touch();
625
        if self.rng.random_ratio(1, 5) {
626
            self.priapism_turns = PRIAPISM_TURNS;
627
            self.eq = self.eq.max(PRIAPISM_EQ);
628
            return Ok(Outcome::json(
629
                StatusCode::OK,
630
                json!({ "detail": "The upstream returned a valid response. It is still returning it." }),
631
            ));
632
        }
633
        Err(Problem::new(
634
            StatusCode::BAD_GATEWAY,
635
            "bad-gateway",
636
            "The upstream returned an invalid response.",
637
        )
638
        .with("upstream", "Pump-Bro"))
639
    }
640
641
    fn edge(&mut self, now: Instant) -> Result<Outcome, Problem> {
642
        self.commit(5, None, now, 1);
643
        self.edges += 1;
644
        self.eq = (self.eq + 11).min(100);
645
        self.girth += 0.004;
646
        if self.edges >= 3 {
647
            self.edges = 0;
648
            self.stamina = 0;
649
            self.refractory_until = Some(now + *REFRACTORY);
650
            return Err(Problem::new(
651
                StatusCode::LOOP_DETECTED,
652
                "edging-loop",
653
                "Edging has looped. The server detected an infinite loop and terminated it.",
654
            ));
655
        }
656
        if let Some(interrupted) = self.hazards(5)? {
657
            return Ok(interrupted);
658
        }
659
        Ok(Outcome::json(
660
            StatusCode::OK,
661
            json!({ "detail": "Edged. EQ is rising. Nothing else is.", "edges": self.edges }),
662
        ))
663
    }
664
665
    fn pump(&mut self, now: Instant) -> Result<Outcome, Problem> {
666
        self.commit(4, None, now, 1);
667
        if let Some(interrupted) = self.hazards(4)? {
668
            return Ok(interrupted);
669
        }
670
        self.temp_length += 0.5;
671
        Ok(Outcome::json(
672
            StatusCode::TEMPORARY_REDIRECT,
673
            json!({ "detail": "Gains applied. They are temporary.", "length": 0.5 }),
674
        )
675
        .header(header::LOCATION, format!("/runs/{}/length", self.id)))
676
    }
677
}