an http based roguelike rpg about jelqing
Commit 139164489032
tarball5 files changed+133-15
@@ -7,8 +7,8 @@ use serde_json::json;
77
use super::{88
Cause, Floor, Outcome, Run,99
balance::{10
- COLD_EQ, CURVE_MISSES, DANGER_EQ, DEGLOVING, DEGLOVING_INTENSITY, FRIEND, HOT_EQ,11
- PREMATURE_EQ, RUPTURE_HOT, RUPTURE_IN_BAND, RUPTURE_PREMATURE, RUPTURE_WARM,10
+ BURN_RUPTURE, COLD_EQ, CURVE_MISSES, DANGER_EQ, DEGLOVING, DEGLOVING_INTENSITY, FRIEND,11
+ HOT_EQ, PREMATURE_EQ, RUPTURE_HOT, RUPTURE_IN_BAND, RUPTURE_PREMATURE, RUPTURE_WARM,1212
SUSPICION_LIMIT, WANDERER_VISIT, WINDOW,1313
},1414
techniques::Training,@@ -28,9 +28,14 @@ impl Run {
2828
2929
/// Everything that can go wrong on a committed turn before the roll.3030
pub(super) fn hazards(&mut self, intensity: i32) -> Interruption {31
- let odds = if self.eq <= DANGER_EQ {31
+ let burn = if self.conditions.friction_burn {32
+ BURN_RUPTURE33
+ } else {34
+ 035
+ };36
+ let odds = if self.eq <= DANGER_EQ - burn {3237
RUPTURE_IN_BAND33
- } else if self.eq <= HOT_EQ {38
+ } else if self.eq <= HOT_EQ - burn {3439
RUPTURE_WARM3540
} else {3641
RUPTURE_HOT@@ -73,7 +78,7 @@ impl Run {
7378
)7479
.header(header::CONNECTION, "close"));7580
}76
- if self.eq > PREMATURE_EQ {81
+ if self.eq > PREMATURE_EQ - burn {7782
if self.chance(RUPTURE_PREMATURE) {7883
return Err(self.rupture());7984
}@@ -36,23 +36,28 @@ impl Run {
3636
self.here.turns += 1;3737
self.realise();3838
let was_below = self.suspicion < SUSPICION_LIMIT;39
+ // One draw of 120 keeps the marginals at the documented 1 in 3, 1 in 5 and 1 in 8.40
+ let draw = self.rng.random_range(0..120);41
+ let oil = 120 / COCONUT_ODDS;42
+ let energy = oil + 120 / ENERGY_ODDS;43
+ let ice = energy + 120 / KITCHEN_ICE_ODDS;3944
let outcome = if self.eq > RUPTURE_LINE && self.rng.random_ratio(WATERMELON.0, WATERMELON.1)4045
{4146
self.here.melon_offered = true;4247
self.melon_offer()43
- } else if self.chance(COCONUT_ODDS) {48
+ } else if draw < oil {4449
self.items.grant(Item::CoconutOil);4550
Ok(Outcome::json(4651
StatusCode::OK,4752
json!({ "detail": "A jar of coconut oil, half of it already gone." }),4853
))49
- } else if self.chance(ENERGY_ODDS) {54
+ } else if draw < energy {5055
self.items.grant(Item::EnergyDrink);5156
Ok(Outcome::json(5257
StatusCode::OK,5358
json!({ "detail": "An energy drink, cold and faintly alarming." }),5459
))55
- } else if self.chance(KITCHEN_ICE_ODDS) {60
+ } else if draw < ice {5661
self.items.grant(Item::IcePack);5762
Ok(Outcome::json(5863
StatusCode::OK,@@ -101,9 +101,9 @@ impl Run {
101101
) -> Result<Outcome, Problem> {102102
self.here.edges = 0;103103
self.ready(Technique::Training(training), lube)?;104
- self.take_lube(lube)?;105104
self.check_storage()?;106105
self.floor_enemy()?;106
+ self.take_lube(lube)?;107107
self.chafe(lube);108108
self.commit(intensity, lube, now, 1);109109
if let ControlFlow::Break(interrupted) = self.mishaps(training, intensity)? {@@ -134,9 +134,9 @@ impl Run {
134134
self.here.edges = 0;135135
self.ready(Technique::Training(a), lube)?;136136
self.ready(Technique::Training(b), lube)?;137
- self.take_lube(lube)?;138137
self.check_storage()?;139138
self.floor_enemy()?;139
+ self.take_lube(lube)?;140140
self.chafe(lube);141141
self.commit(intensity, lube, now, 2);142142
for training in [a, b] {@@ -268,7 +268,7 @@ impl Run {
268268
};269269
if *held == 0 {270270
return Err(Problem::new(271
- StatusCode::PRECONDITION_FAILED,271
+ StatusCode::UNSUPPORTED_MEDIA_TYPE,272272
"lubricant-exhausted",273273
format!("{name} was declared, but the bottle is empty."),274274
));@@ -482,7 +482,12 @@ impl Run {
482482
));483483
}484484
self.here.meditated = true;485
- self.eq = (self.eq - MEDITATION_EQ).clamp(0, 100);485
+ let floor = if self.conditions.priapism_turns > 0 {486
+ PRIAPISM_EQ487
+ } else {488
+ 0489
+ };490
+ self.eq = (self.eq - MEDITATION_EQ).max(floor);486491
self.shed(FRICTION_MEDITATE);487492
self.touch();488493
Ok(Outcome::json(@@ -662,7 +667,12 @@ impl Run {
662667
)663668
.header(header::CONTENT_LOCATION, self.path("/foreskin"));664669
}665
- if self.eq > DANGER_EQ {670
+ let burn = if self.conditions.friction_burn {671
+ BURN_RUPTURE672
+ } else {673
+ 0674
+ };675
+ if self.eq > DANGER_EQ - burn {666676
return self.rupture();667677
}668678
if training == Training::Stretch && !self.here.teapot {@@ -14,7 +14,10 @@ use tower::ServiceExt;
1414
1515
use crate::{1616
AppState, Config,17
- game::{Floor, MumVisit, Run, Story, balance::BRANCH_TURN, balance::SUSPICION_LIMIT},17
+ game::{18
+ Floor, MumVisit, Run, Story,19
+ balance::{BRANCH_TURN, STORAGE_GIRTH, SUSPICION_LIMIT},20
+ },1821
router,1922
};2023
@@ -1246,7 +1249,7 @@ async fn lubes_run_out_but_spit_never_does() {
12461249
let first = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;12471250
assert_ne!(first.status(), StatusCode::PRECONDITION_FAILED);12481251
let second = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;1249
- assert_eq!(second.status(), StatusCode::PRECONDITION_FAILED);1252
+ assert_eq!(second.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);12501253
12511254
let spit = [("content-type", "text/plain; charset=spit")];12521255
for _ in 0..3 {@@ -1438,3 +1441,96 @@ async fn the_burn_warns_and_the_ice_treats_it() {
14381441
.collect();14391442
assert!(!warnings.contains(&"199 - \"friction burn\""));14401443
}1444
+1445
+#[tokio::test]1446
+async fn the_burn_shifts_the_rupture_tiers() {1447
+ async fn ruptures(burned: bool) -> u32 {1448
+ let mut gone = 0;1449
+ for seed in 0..400u64 {1450
+ let app = app_with(Floor::Bedroom, seed, |run| {1451
+ run.warmed_up = true;1452
+ run.eq = 60;1453
+ run.items.baby_oil = 9;1454
+ run.conditions.friction_burn = burned;1455
+ });1456
+ let lube = [("content-type", "application/baby-oil")];1457
+ let jelq = send(1458
+ &app,1459
+ "JELQ",1460
+ "/runs/cafebabe?technique=wet&intensity=5",1461
+ &lube,1462
+ "",1463
+ )1464
+ .await;1465
+ if jelq.status() == StatusCode::INTERNAL_SERVER_ERROR {1466
+ gone += 1;1467
+ }1468
+ }1469
+ gone1470
+ }1471
+ let burned = ruptures(true).await;1472
+ let calm = ruptures(false).await;1473
+ assert!(1474
+ burned >= 8 && calm <= 4,1475
+ "burned {burned} ruptures against {calm}"1476
+ );1477
+}1478
+1479
+#[tokio::test]1480
+async fn meditation_respects_the_priapism_floor() {1481
+ let app = seeded_with(Floor::Bedroom, |run| {1482
+ run.meditation_taught = true;1483
+ run.eq = 80;1484
+ run.conditions.priapism_turns = 3;1485
+ });1486
+ let calmed = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;1487
+ assert_eq!(calmed.status(), StatusCode::OK);1488
+ let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;1489
+ assert_eq!(header(&view, "x-eq"), "75");1490
+}1491
+1492
+#[tokio::test]1493
+async fn the_kitchen_pays_out_at_the_documented_odds() {1494
+ let mut oil = 0;1495
+ let mut drinks = 0;1496
+ let mut packs = 0;1497
+ for seed in 0..600u64 {1498
+ let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);1499
+ run.kitchen_tellings = 0;1500
+ let held = (1501
+ run.items.coconut_oil,1502
+ run.items.energy_drinks,1503
+ run.items.ice_packs,1504
+ );1505
+ let _ = run.raid_kitchen();1506
+ oil += u32::from(run.items.coconut_oil > held.0);1507
+ drinks += u32::from(run.items.energy_drinks > held.1);1508
+ packs += u32::from(run.items.ice_packs > held.2);1509
+ }1510
+ assert!((170..=230).contains(&oil), "coconut {oil} in 600");1511
+ assert!((90..=150).contains(&drinks), "energy {drinks} in 600");1512
+ assert!((45..=105).contains(&packs), "ice {packs} in 600");1513
+}1514
+1515
+#[tokio::test]1516
+async fn a_refused_jelq_leaves_the_lube_alone() {1517
+ let app = seeded_with(Floor::Bedroom, |run| {1518
+ run.warmed_up = true;1519
+ run.items.extender = true;1520
+ run.girth = STORAGE_GIRTH + 1.0;1521
+ });1522
+ let lube = [("content-type", "application/baby-oil")];1523
+ let full = send(1524
+ &app,1525
+ "JELQ",1526
+ "/runs/cafebabe?technique=wet&intensity=5",1527
+ &lube,1528
+ "",1529
+ )1530
+ .await;1531
+ assert_eq!(full.status(), StatusCode::INSUFFICIENT_STORAGE);1532
+ let inv = send(&app, "GET", "/runs/cafebabe/inventory", &[], "").await;1533
+ let body = json(inv).await;1534
+ let held = body["items"].as_array().unwrap();1535
+ assert!(held.iter().any(|i| i["name"] == "baby-oil"));1536
+}@@ -395,9 +395,11 @@ function absorb(r, sentMethod, sentPath) {
395395
}396396
if (headers.link) [linked, techniques] = parseLinks(headers.link);397397
if (run) {398
+ const active = run;398399
fetch("/ui/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ method: "GET", path: run + "/inventory", headers: [], body: "" }) })399400
.then(r => r.json())400401
.then(r => {402
+ if (run !== active) return;401403
try { stocked = (JSON.parse(r.body).items || []).map(i => i.name); render(); } catch (e) {}402404
});403405
}