an http based roguelike rpg about jelqing
Commit 14de3e1e8dc2
tarball7 files changed+141-22
@@ -23,7 +23,7 @@ impl Run {
2323
..FloorState::default()2424
};2525
self.warmed_up = false;26
- self.eq = STARTING_EQ;26
+ self.reset_eq(STARTING_EQ);2727
self.suspicion = 0;2828
self.shed(FRICTION_FLOOR);2929
self.mum = None;@@ -33,14 +33,16 @@ impl Run {
3333
} else {3434
03535
};36
- let odds = if self.eq <= DANGER_EQ - burn {37
- RUPTURE_IN_BAND36
+ let odds = if self.eq > PREMATURE_EQ - burn {37
+ None38
+ } else if self.eq <= DANGER_EQ - burn {39
+ Some(RUPTURE_IN_BAND)3840
} else if self.eq <= HOT_EQ - burn {39
- RUPTURE_WARM41
+ Some(RUPTURE_WARM)4042
} else {41
- RUPTURE_HOT43
+ Some(RUPTURE_HOT)4244
};43
- if self.chance(odds) {45
+ if odds.is_some_and(|odds| self.chance(odds)) {4446
return Err(self.rupture());4547
}4648
if self.floor.has_window() && !self.items.blinds && self.chance(WINDOW) {@@ -116,7 +116,7 @@ impl Run {
116116
));117117
}118118
self.here.melon_offered = false;119
- self.eq = 0;119
+ self.reset_eq(0);120120
self.shame += MELON_SHAME;121121
self.touch();122122
if self.chance(CAUGHT_MELONED) {@@ -34,8 +34,8 @@ use serde_json::{Value, json};
3434
3535
use crate::problem::Problem;3636
use balance::{37
- HONEST_TOLERANCE, MAX_STAMINA, PULLED_STAMINA, SELF_REPORT_INFLATION, STARTING_CASH,38
- STARTING_EQ,37
+ HONEST_TOLERANCE, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, SELF_REPORT_INFLATION,38
+ STARTING_CASH, STARTING_EQ,3939
};4040
use state::Pending;4141
@@ -197,6 +197,16 @@ impl Run {
197197
self.friction = (self.friction - friction).max(0);198198
}199199
200
+ /// Priapism holds EQ up no matter what else moves it.201
+ pub(super) fn reset_eq(&mut self, eq: i32) {202
+ let floor = if self.conditions.priapism_turns > 0 {203
+ PRIAPISM_EQ204
+ } else {205
+ 0206
+ };207
+ self.eq = eq.max(floor);208
+ }209
+200210
fn fresh_nonce(&mut self) -> String {201211
format!("{:016x}", self.rng.random::<u64>())202212
}@@ -281,7 +291,7 @@ impl Run {
281291
if self.refractory_until.is_some_and(|until| now >= until) {282292
self.refractory_until = None;283293
self.stamina = self.max_stamina();284
- self.eq = STARTING_EQ;294
+ self.reset_eq(STARTING_EQ);285295
self.warmed_up = false;286296
self.touch();287297
}@@ -102,6 +102,7 @@ impl Run {
102102
self.here.edges = 0;103103
self.ready(Technique::Training(training), lube)?;104104
self.check_storage()?;105
+ self.lube_available(lube)?;105106
self.floor_enemy()?;106107
self.take_lube(lube)?;107108
self.chafe(lube);@@ -135,6 +136,7 @@ impl Run {
135136
self.ready(Technique::Training(a), lube)?;136137
self.ready(Technique::Training(b), lube)?;137138
self.check_storage()?;139
+ self.lube_available(lube)?;138140
self.floor_enemy()?;139141
self.take_lube(lube)?;140142
self.chafe(lube);@@ -258,13 +260,13 @@ impl Run {
258260
}259261
260262
/// One unit of whatever was declared. Spit is free and the tap is always on.261
- fn take_lube(&mut self, lube: Option<Lube>) -> Result<(), Problem> {263
+ fn lube_available(&self, lube: Option<Lube>) -> Result<(), Problem> {262264
let (held, name) = match lube {263265
None | Some(Lube::Spit) => return Ok(()),264
- Some(Lube::BabyOil) => (&mut self.items.baby_oil, "baby-oil"),265
- Some(Lube::CoconutOil) => (&mut self.items.coconut_oil, "coconut-oil"),266
- Some(Lube::Lotion) => (&mut self.items.lotion, "lotion"),267
- Some(Lube::Vaseline) => (&mut self.items.vaseline, "vaseline"),266
+ Some(Lube::BabyOil) => (&self.items.baby_oil, "baby-oil"),267
+ Some(Lube::CoconutOil) => (&self.items.coconut_oil, "coconut-oil"),268
+ Some(Lube::Lotion) => (&self.items.lotion, "lotion"),269
+ Some(Lube::Vaseline) => (&self.items.vaseline, "vaseline"),268270
};269271
if *held == 0 {270272
return Err(Problem::new(@@ -273,6 +275,18 @@ impl Run {
273275
format!("{name} was declared, but the bottle is empty."),274276
));275277
}278
+ Ok(())279
+ }280
+281
+ fn take_lube(&mut self, lube: Option<Lube>) -> Result<(), Problem> {282
+ self.lube_available(lube)?;283
+ let held = match lube {284
+ None | Some(Lube::Spit) => return Ok(()),285
+ Some(Lube::BabyOil) => &mut self.items.baby_oil,286
+ Some(Lube::CoconutOil) => &mut self.items.coconut_oil,287
+ Some(Lube::Lotion) => &mut self.items.lotion,288
+ Some(Lube::Vaseline) => &mut self.items.vaseline,289
+ };276290
*held -= 1;277291
Ok(())278292
}@@ -482,12 +496,7 @@ impl Run {
482496
));483497
}484498
self.here.meditated = true;485
- let floor = if self.conditions.priapism_turns > 0 {486
- PRIAPISM_EQ487
- } else {488
- 0489
- };490
- self.eq = (self.eq - MEDITATION_EQ).max(floor);499
+ self.reset_eq(self.eq - MEDITATION_EQ);491500
self.shed(FRICTION_MEDITATE);492501
self.touch();493502
Ok(Outcome::json(@@ -1534,3 +1534,99 @@ async fn a_refused_jelq_leaves_the_lube_alone() {
15341534
let held = body["items"].as_array().unwrap();15351535
assert!(held.iter().any(|i| i["name"] == "baby-oil"));15361536
}1537
+1538
+#[tokio::test]1539
+async fn an_empty_bottle_is_declined_before_the_enemy_stirs() {1540
+ let control = seeded_with(Floor::ChangingRoom, |run| {1541
+ run.warmed_up = true;1542
+ run.suspicion = SUSPICION_LIMIT;1543
+ });1544
+ let lube = [("content-type", "application/baby-oil")];1545
+ let caught = send(1546
+ &control,1547
+ "JELQ",1548
+ "/runs/cafebabe?technique=wet&intensity=5",1549
+ &lube,1550
+ "",1551
+ )1552
+ .await;1553
+ assert_eq!(caught.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);1554
+1555
+ let app = seeded_with(Floor::ChangingRoom, |run| {1556
+ run.warmed_up = true;1557
+ run.suspicion = SUSPICION_LIMIT;1558
+ run.items.baby_oil = 0;1559
+ });1560
+ let refused = send(1561
+ &app,1562
+ "JELQ",1563
+ "/runs/cafebabe?technique=wet&intensity=5",1564
+ &lube,1565
+ "",1566
+ )1567
+ .await;1568
+ assert_eq!(refused.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);1569
+ let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;1570
+ assert_eq!(header(&view, "x-suspicion"), "5/5");1571
+ assert_eq!(header(&view, "x-shame"), "0");1572
+}1573
+1574
+#[tokio::test]1575
+async fn the_top_tier_rolls_a_single_third() {1576
+ let mut ruptures = 0;1577
+ let mut early = 0;1578
+ for seed in 0..400u64 {1579
+ let app = app_with(Floor::Bedroom, seed, |run| {1580
+ run.warmed_up = true;1581
+ run.eq = 96;1582
+ run.items.baby_oil = 9;1583
+ run.items.blinds = true;1584
+ });1585
+ let lube = [("content-type", "application/baby-oil")];1586
+ let jelq = send(1587
+ &app,1588
+ "JELQ",1589
+ "/runs/cafebabe?technique=wet&intensity=5",1590
+ &lube,1591
+ "",1592
+ )1593
+ .await;1594
+ match jelq.status() {1595
+ StatusCode::INTERNAL_SERVER_ERROR => ruptures += 1,1596
+ StatusCode::TOO_EARLY => early += 1,1597
+ other => panic!("seed {seed} ended in {other}"),1598
+ }1599
+ }1600
+ assert!(1601
+ (90..=175).contains(&ruptures),1602
+ "ruptures {ruptures} of 400 against {early} early"1603
+ );1604
+ assert!(1605
+ (225..=310).contains(&early),1606
+ "early {early} of 400 against {ruptures} ruptures"1607
+ );1608
+}1609
+1610
+#[tokio::test]1611
+async fn priapism_holds_the_floor_through_every_reset() {1612
+ let exit = seeded_with(Floor::Bedroom, |run| {1613
+ run.warmed_up = true;1614
+ run.eq = 90;1615
+ run.here.turns = 99;1616
+ run.conditions.priapism_turns = 3;1617
+ });1618
+ let out = send(&exit, "POST", "/runs/cafebabe/floor", &[], "").await;1619
+ assert_eq!(out.status(), StatusCode::SEE_OTHER);1620
+ let view = send(&exit, "GET", "/runs/cafebabe", &[], "").await;1621
+ assert_eq!(header(&view, "x-eq"), "75");1622
+1623
+ let melon = seeded_with(Floor::Bedroom, |run| {1624
+ run.eq = 95;1625
+ run.here.melon_offered = true;1626
+ run.conditions.priapism_turns = 2;1627
+ });1628
+ let bite = send(&melon, "POST", "/runs/cafebabe/kitchen/watermelon", &[], "").await;1629
+ assert_eq!(bite.status(), StatusCode::OK);1630
+ let view = send(&melon, "GET", "/runs/cafebabe", &[], "").await;1631
+ assert_eq!(header(&view, "x-eq"), "75");1632
+}@@ -60,6 +60,7 @@ const METHODS = ["JELQ", "HOLD", "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"
6060
const ALL_LUBES = ["", "application/baby-oil", "application/coconut-oil", "application/lotion", "application/vaseline", "text/plain; charset=spit", "image/jpeg"];6161
const DEATHS = ["vascular-event", "caught", "opened", "banned", "degloved", "zip", "watermelon", "run-ended"];6262
let stocked = null;63
+let inventorySeq = 0;6364
const STATS = [6465
["x-floor", "Floor"],6566
["x-self-reported-length", "Self-reported length"],@@ -396,10 +397,11 @@ function absorb(r, sentMethod, sentPath) {
396397
if (headers.link) [linked, techniques] = parseLinks(headers.link);397398
if (run) {398399
const active = run;400
+ const seq = ++inventorySeq;399401
fetch("/ui/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ method: "GET", path: run + "/inventory", headers: [], body: "" }) })400402
.then(r => r.json())401403
.then(r => {402
- if (run !== active) return;404
+ if (run !== active || seq !== inventorySeq) return;403405
try { stocked = (JSON.parse(r.body).items || []).map(i => i.name); render(); } catch (e) {}404406
});405407
}