an http based roguelike rpg about jelqing
Commit e028a4184d00
tarball17 files changed+211-62
@@ -0,0 +1,66 @@
1
+use super::{2
+ Run,3
+ balance::{DANGER_EQ, EQ_BAND, HOT_EQ, PREMATURE_EQ, RING_RUPTURE_LINE, RUPTURE_LINE},4
+};5
+6
+/// What `X-EQ` shows in place of the number, cut on the lines the rolls already use.7
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]8
+pub enum Arousal {9
+ Flaccid,10
+ Cold,11
+ Warm,12
+ Ideal,13
+ Swollen,14
+ Throbbing,15
+ Straining,16
+ Imminent,17
+ Priapic,18
+}19
+20
+impl Arousal {21
+ pub fn name(self) -> &'static str {22
+ match self {23
+ Self::Flaccid => "Flaccid",24
+ Self::Cold => "Cold",25
+ Self::Warm => "Warm",26
+ Self::Ideal => "Ideal",27
+ Self::Swollen => "Swollen",28
+ Self::Throbbing => "Throbbing",29
+ Self::Straining => "Straining",30
+ Self::Imminent => "Imminent",31
+ Self::Priapic => "Priapic",32
+ }33
+ }34
+}35
+36
+impl Run {37
+ pub(super) fn rupture_line(&self) -> i32 {38
+ if self.items.kit.ring {39
+ RING_RUPTURE_LINE40
+ } else {41
+ RUPTURE_LINE42
+ }43
+ }44
+45
+ pub fn arousal(&self) -> Arousal {46
+ if self.body.conditions.priapism_turns > 0 {47
+ Arousal::Priapic48
+ } else if self.hotter_than(PREMATURE_EQ) {49
+ Arousal::Imminent50
+ } else if self.hotter_than(self.rupture_line()) {51
+ Arousal::Straining52
+ } else if self.hotter_than(HOT_EQ) {53
+ Arousal::Throbbing54
+ } else if self.hotter_than(DANGER_EQ) {55
+ Arousal::Swollen56
+ } else if EQ_BAND.contains(&self.body.eq) {57
+ Arousal::Ideal58
+ } else if self.body.eq == 0 {59
+ Arousal::Flaccid60
+ } else if self.cold() {61
+ Arousal::Cold62
+ } else {63
+ Arousal::Warm64
+ }65
+ }66
+}@@ -33,7 +33,7 @@ impl Cause {
3333
3434
pub fn slug(self) -> &'static str {3535
match self {36
- Self::Ruptured { .. } => "vascular-event",36
+ Self::Ruptured { .. } => "ruptured",3737
Self::Caught => "caught",3838
Self::Opened => "opened",3939
Self::Banned => "banned",@@ -42,7 +42,7 @@ pub fn forum_thread() -> Value {
4242
"title": "The definitive jelqing FAQ (updated 2009)",4343
"posts": [4444
{ "author": "TheElder", "body": "Warm up. Always warm up. A cold jelq is a 412 waiting to happen." },45
- { "author": "TheElder", "body": "Keep EQ between 50 and 70. Above 70 you are gambling. Above 90 you are a cautionary tale." },45
+ { "author": "TheElder", "body": "Keep it Ideal. Swollen is gambling. Straining is a cautionary tale." },4646
{ "author": "newbie_2009", "body": "everyone keeps saying uli. who is uli" },4747
{ "author": "TheElder", "body": "uli was a member here. He posted the technique in 2001 and never posted again. Some say he ascended. The search function says his account is gone." },4848
{ "author": "TheElder", "body": "We do not say his name in vain. We say it in girth." },@@ -3,7 +3,7 @@ use std::time::Instant;
33
use serde_json::{Value, json};44
55
use super::{6
- Arrival, Elder, Excuse, Floor, Grip, Item, Observer, Run, Story, Technique,6
+ Arousal, Arrival, Elder, Excuse, Floor, Grip, Item, Observer, Run, Story, Technique,77
balance::DANGER_EQ,88
techniques::{Training, Utility},99
};@@ -18,7 +18,7 @@ pub struct Gauges {
1818
pub(crate) girth: f64,1919
pub(crate) foreskin: Option<f64>,2020
pub(crate) friction: i32,21
- pub(crate) eq: i32,21
+ pub(crate) arousal: Arousal,2222
pub(crate) stamina: i32,2323
pub(crate) max_stamina: i32,2424
pub(crate) suspicion: u8,@@ -62,7 +62,7 @@ impl Run {
6262
if arrived.is_some() {6363
warnings.push("the queued gains have arrived");6464
}65
- if self.body.eq > DANGER_EQ && self.ended.is_none() {65
+ if self.hotter_than(DANGER_EQ) && self.ended.is_none() {6666
warnings.push("it is turning purple");6767
}6868
Gauges {@@ -74,7 +74,7 @@ impl Run {
7474
girth: self.body.girth,7575
foreskin: self.body.foreskin,7676
friction: self.body.friction,77
- eq: self.body.eq,77
+ arousal: self.arousal(),7878
stamina: self.body.stamina,7979
max_stamina: self.max_stamina(),8080
suspicion: self.standing.suspicion,@@ -98,7 +98,7 @@ impl Item {
9898
Self::WarmTowel => {9999
"Reusable. Satisfies the warm-up precondition. Use technique=warmup."100100
}101
- Self::IcePack => "Lowers EQ by 40. Use technique=ice.",101
+ Self::IcePack => "Cools things sharply. Use technique=ice.",102102
Self::Ruler => "Measures honestly. The length resource now carries a Content-Length.",103103
Self::DoorLock => "Keeps your brother out once.",104104
Self::DndSign => "Works on housekeeping. Mum does not read signs.",@@ -1,3 +1,4 @@
1
+mod arousal;12
pub mod balance;23
mod body;34
mod cause;@@ -29,6 +30,7 @@ mod techniques;
2930
mod travel;3031
mod utility;3132
33
+pub use arousal::Arousal;3234
#[cfg(test)]3335
pub use body::Pending;3436
pub use body::{Arrival, Body, Grip};@@ -11,7 +11,7 @@ use serde_json::json;
1111
1212
impl Run {1313
pub fn can_hold(&self) -> bool {14
- self.body.eq > DANGER_EQ || self.body.stamina <= 214
+ self.hotter_than(DANGER_EQ) || self.body.stamina <= 21515
}1616
1717
pub fn hold(&mut self) -> Result<Outcome, Problem> {@@ -95,7 +95,7 @@ impl Run {
9595
Ok(Outcome::json(9696
StatusCode::OK,9797
json!({98
- "detail": "In through the nose, out through the mouth. EQ falls by ten.",98
+ "detail": "In through the nose, out through the mouth. EQ falls a little.",9999
}),100100
))101101
}@@ -2,7 +2,7 @@ use super::{
22
Grip, Lube, Outcome, Run,33
balance::{44
ARRIVAL_BONUS, BAND_BONUS, CLEAN_HIT, DANGER_EQ, EQ_BAND, GREED_HOT, GREED_WARM, HOT_EQ,5
- NEAR_MISS, QUEUED_ETA, RING_RUPTURE_LINE, RUPTURE_LINE, UNDERHAND_STRETCH, UNDERHAND_WET,5
+ NEAR_MISS, QUEUED_ETA, UNDERHAND_STRETCH, UNDERHAND_WET,66
},77
body::Pending,88
measure::round3,@@ -43,11 +43,7 @@ impl Run {
4343
let timed = |p: Problem| p.header(SERVER_TIMING, &timing);4444
4545
let (length, girth) = self.scaled_gains(&profile, intensity);46
- let rupture_line = if self.items.kit.ring {47
- RING_RUPTURE_LINE48
- } else {49
- RUPTURE_LINE50
- };46
+ let rupture_line = self.rupture_line();5147
5248
if natural == 1 {5349
return Err(timed(self.critical_failure(training, &profile)));@@ -109,7 +105,7 @@ impl Run {
109105
Outcome::json(110106
StatusCode::PARTIAL_CONTENT,111107
gains(112
- "Partial gains were applied. EQ was below the band.",108
+ "Partial gains were applied. EQ was short of Ideal.",113109
length / 2.0,114110
girth / 2.0,115111
foreskin,@@ -102,12 +102,16 @@ impl Utility {
102102
103103
fn effect(self) -> &'static str {104104
match self {105
- Self::Warmup => "EQ +25. Satisfies the warm-up precondition.",106
- Self::Ice => "EQ -40. Treats a friction burn. Consumes an ice pack.",107
- Self::Edge => "EQ +15 and a little girth. Three in a row is a loop.",105
+ Self::Warmup => "Warms things a good deal. Satisfies the warm-up precondition.",106
+ Self::Ice => "Cools things sharply. Treats a friction burn. Consumes an ice pack.",107
+ Self::Edge => {108
+ "Warms things a little and adds a little girth. Three in a row is a loop."109
+ }108110
Self::Pump => "Large gains, all temporary. Requires a pump.",109111
Self::Supplement => "Consumes a supplement.",110
- Self::Meditate => "EQ -10, sheds 2 friction. Taught by the forum. Once per floor.",112
+ Self::Meditate => {113
+ "Cools things a little, sheds 2 friction. Taught by the forum. Once per floor."114
+ }111115
Self::Energy => "+3 stamina. Consumes an energy drink. Tastes like a battery.",112116
}113117
}@@ -142,6 +146,14 @@ impl Technique {
142146
pub fn lubricant(self) -> Option<&'static str> {143147
match self {144148
Self::Training(training) => Some(training.profile().lube.name()),149
+ Self::Utility(Utility::Edge) => Some(LubeRule::Optional.name()),150
+ Self::Utility(_) => None,151
+ }152
+ }153
+154
+ pub fn intensity(self) -> Option<&'static str> {155
+ match self {156
+ Self::Training(_) => Some("1-10"),145157
Self::Utility(_) => None,146158
}147159
}@@ -152,6 +164,7 @@ impl Technique {
152164
return json!({153165
"name": utility.name(),154166
"effect": utility.effect(),167
+ "lubricant": self.lubricant(),155168
"locked": !known,156169
});157170
}@@ -5,7 +5,7 @@ use crate::game::{Branch, Run, SHOP};
55
66
pub const REL_BASE: &str = "https://jelq.dev/rels/";77
8
-/// One RFC 8288 link. `allow` and `lubricant` are extension attributes (§3), so a client never8
+/// One RFC 8288 link. `allow`, `lubricant` and `intensity` are extension attributes (§3), so a client never99
/// has to guess what a target accepts.1010
struct Link<'a> {1111
href: &'a str,@@ -13,6 +13,7 @@ struct Link<'a> {
1313
allow: &'a str,1414
title: Option<&'a str>,1515
lubricant: Option<&'a str>,16
+ intensity: Option<&'a str>,1617
}1718
1819
impl Link<'_> {@@ -28,6 +29,9 @@ impl Link<'_> {
2829
if let Some(rule) = self.lubricant {2930
let _ = write!(out, "; lubricant=\"{rule}\"");3031
}32
+ if let Some(range) = self.intensity {33
+ let _ = write!(out, "; intensity=\"{range}\"");34
+ }3135
}3236
}3337
@@ -63,6 +67,7 @@ pub fn header(run: &Run) -> String {
6367
allow: &target.resource.allow(run),6468
title: None,6569
lubricant: None,70
+ intensity: None,6671
}6772
.write(&mut out);6873
}@@ -77,6 +82,7 @@ pub fn header(run: &Run) -> String {
7782
allow: "POST",7883
title: Some(title),7984
lubricant: None,85
+ intensity: None,8086
}8187
.write(&mut out);8288
}@@ -90,6 +96,7 @@ pub fn header(run: &Run) -> String {
9096
allow: "JELQ",9197
title: Some(name),9298
lubricant: technique.lubricant(),99
+ intensity: technique.intensity(),93100
}94101
.write(&mut out);95102
}@@ -104,6 +111,7 @@ pub fn header(run: &Run) -> String {
104111
allow: "POST",105112
title: Some(&format!("{} for {price}", item.slug())),106113
lubricant: None,114
+ intensity: None,107115
}108116
.write(&mut out);109117
}@@ -71,6 +71,11 @@ impl AppState {
7171
pub(crate) fn insert(&self, run: crate::game::Run) {7272
self.runs.insert_new(run);7373
}74
+75
+ #[cfg(test)]76
+ pub(crate) fn inspect<T>(&self, visit: impl FnMut(&crate::game::Run) -> T) -> Vec<T> {77
+ self.runs.each(visit)78
+ }7479
}7580
7681
impl Default for AppState {@@ -152,7 +152,7 @@ fn stats(gauges: &Gauges, shame: u32) -> Vec<(HeaderName, String)> {
152152
gauges.foreskin.map_or("gone".into(), |f| format!("{f:.2}")),153153
),154154
(X_FRICTION, format!("{}/{FRICTION_MAX}", gauges.friction)),155
- (X_EQ, gauges.eq.to_string()),155
+ (X_EQ, gauges.arousal.name().into()),156156
(157157
X_STAMINA,158158
format!("{}/{}", gauges.stamina, gauges.max_stamina),@@ -19,7 +19,7 @@ use super::{
1919
use crate::{2020
game::{2121
Branch, Cause, Floor, Grip, Item, LUBRICANTS, Lube, Run, Technique,22
- balance::{EQ_BAND, VIEWS_PER_SECOND},22
+ balance::VIEWS_PER_SECOND,2323
},2424
problem::Problem,2525
};@@ -49,7 +49,7 @@ pub static RUN: RunResource = RunResource {
4949
Verb::new("JELQ", jelq).only_when(not_in_clinic, "JELQ is not performed in the clinic."),5050
Verb::new("HOLD", hold_run).only_when(5151
Run::can_hold,52
- "HOLD is only available above 70 EQ or at 2 stamina or less.",52
+ "HOLD is only available once Swollen or at 2 stamina or less.",5353
),5454
Verb::new("PUT", put_run),5555
Verb::new("DELETE", delete_run),@@ -157,7 +157,6 @@ fn get_techniques(run: &mut Run, _: &Ctx) -> Response {
157157
"usage": format!("JELQ {}?technique=<name>[,<name>]&intensity=<1-10>", run.path("")),158158
"lubricant": "Declared with Content-Type.",159159
"lubricants": LUBRICANTS,160
- "eq_band": [EQ_BAND.start(), EQ_BAND.end()],161160
"techniques": listed,162161
}))163162
.into_response()@@ -107,7 +107,16 @@ async fn links_offer_only_usable_techniques() {
107107
assert!(!usable.contains(&"uli".to_owned()), "uli needs the forum");108108
assert!(!usable.contains(&"pump".to_owned()), "pump needs a pump");109109
assert!(110
- header(&warm, "link").contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"")110
+ header(&warm, "link")111
+ .contains("title=\"wet\"; allow=\"JELQ\"; lubricant=\"required\"; intensity=\"1-10\"")112
+ );113
+ let edge = header(&warm, "link")114
+ .split(", ")115
+ .find(|link| link.contains("title=\"edge\""))116
+ .unwrap();117
+ assert!(118
+ edge.ends_with("allow=\"JELQ\"; lubricant=\"optional\""),119
+ "an edge takes lube but no intensity"111120
);112121
113122
let Harness { app: forum, .. } = harness(Floor::Forum, SEED, |_| {});@@ -79,6 +79,14 @@ pub fn warnings(response: &Response) -> Vec<String> {
7979
pub struct Harness {8080
pub app: Router,8181
pub origin: String,82
+ pub state: Arc<AppState>,83
+}84
+85
+impl Harness {86
+ /// The EQ of `cafebabe`, which `X-EQ` only names.87
+ pub fn eq(&self) -> i32 {88
+ self.state.inspect(|run| run.body.eq)[0]89
+ }8290
}8391
8492
pub fn harness(floor: Floor, seed: u64, setup: impl FnOnce(&mut Run)) -> Harness {@@ -89,8 +97,9 @@ pub fn harness(floor: Floor, seed: u64, setup: impl FnOnce(&mut Run)) -> Harness
8997
let origin = run.origin_etag();9098
state.insert(run);9199
Harness {92
- app: router(state),100
+ app: router(state.clone()),93101
origin,102
+ state,94103
}95104
}96105
@@ -2,49 +2,65 @@ use super::*;
22
33
#[tokio::test]44
async fn meditation_is_free_but_only_once_per_floor() {5
- let app = seeded_with(Floor::Bedroom, |run| run.online.meditation_taught = true);6
- let response = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;5
+ let held = harness(Floor::Bedroom, SEED, |run| {6
+ run.online.meditation_taught = true7
+ });8
+ let app = &held.app;9
+ let response = send(app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;710
assert_eq!(response.status(), StatusCode::OK);8
- assert_eq!(header(&response, "x-eq"), "10");11
+ assert_eq!(header(&response, "x-eq"), "Cold");12
+ assert_eq!(held.eq(), 10);913
assert_eq!(header(&response, "x-stamina"), "10/10");10
- let again = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;14
+ let again = send(app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;1115
assert_eq!(again.status(), StatusCode::CONFLICT);1216
}1317
1418
#[tokio::test]1519
async fn meditation_respects_the_priapism_floor() {16
- let app = seeded_with(Floor::Bedroom, |run| {20
+ let held = harness(Floor::Bedroom, SEED, |run| {1721
run.body.eq = 80;1822
run.body.conditions.priapism_turns = 3;1923
run.online.meditation_taught = true;2024
});21
- let calmed = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;25
+ let calmed = send(26
+ &held.app,27
+ "JELQ",28
+ "/runs/cafebabe?technique=meditate",29
+ &[],30
+ "",31
+ )32
+ .await;2233
assert_eq!(calmed.status(), StatusCode::OK);23
- let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;24
- assert_eq!(header(&view, "x-eq"), "75");34
+ assert_eq!(header(&calmed, "x-eq"), "Priapic");35
+ assert_eq!(held.eq(), 75);2536
}2637
2738
#[tokio::test]2839
async fn priapism_holds_the_floor_through_every_reset() {29
- let exit = seeded_with(Floor::Bedroom, |run| {40
+ let exit = harness(Floor::Bedroom, SEED, |run| {3041
run.body.eq = 90;3142
run.here.turns = 99;3243
run.body.conditions.priapism_turns = 3;3344
});34
- let out = send(&exit, "POST", "/runs/cafebabe/floor", &[], "").await;45
+ let out = send(&exit.app, "POST", "/runs/cafebabe/floor", &[], "").await;3546
assert_eq!(out.status(), StatusCode::SEE_OTHER);36
- let view = send(&exit, "GET", "/runs/cafebabe", &[], "").await;37
- assert_eq!(header(&view, "x-eq"), "75");47
+ assert_eq!(exit.eq(), 75);3848
39
- let melon = seeded_with(Floor::Bedroom, |run| {49
+ let melon = harness(Floor::Bedroom, SEED, |run| {4050
run.body.eq = 95;4151
run.here.kitchen.melon_offered = true;4252
run.body.conditions.priapism_turns = 2;4353
});44
- let bite = send(&melon, "POST", "/runs/cafebabe/kitchen/watermelon", &[], "").await;54
+ let bite = send(55
+ &melon.app,56
+ "POST",57
+ "/runs/cafebabe/kitchen/watermelon",58
+ &[],59
+ "",60
+ )61
+ .await;4562
assert_eq!(bite.status(), StatusCode::OK);46
- let view = send(&melon, "GET", "/runs/cafebabe", &[], "").await;47
- assert_eq!(header(&view, "x-eq"), "75");63
+ assert_eq!(melon.eq(), 75);4864
}4965
5066
#[tokio::test]@@ -261,7 +277,7 @@ async fn an_energy_drink_ends_exhaustion_without_the_refractory_period() {
261277
assert_eq!(header(&drink, "x-stamina"), "3/10");262278
assert_eq!(263279
header(&drink, "x-eq"),264
- "60",280
+ "Ideal",265281
"still warm, unlike the refractory HOLD"266282
);267283
let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;@@ -286,29 +302,26 @@ async fn overstaying_a_floor_is_warned_then_punished() {
286302
let mut busted = 0;287303
let mut moved = 0;288304
for seed in 0..60u64 {289
- let app = app_with(Floor::ChangingRoom, seed, |run| {305
+ let held = harness(Floor::ChangingRoom, seed, |run| {290306
run.body.eq = 80;291307
run.body.conditions.priapism_turns = 9;292308
run.here.turns = 7;293309
});294
- let eighth = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;310
+ let app = &held.app;311
+ let eighth = send(app, "HOLD", "/runs/cafebabe", &[], "").await;295312
assert!(296313
warnings(&eighth)297314
.iter()298315
.any(|w| w.contains("The gym closes"))299316
);300
- let ninth = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;317
+ let ninth = send(app, "HOLD", "/runs/cafebabe", &[], "").await;301318
assert!(warnings(&ninth).iter().any(|w| w.contains("last turn")));302
- let tenth = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;319
+ let tenth = send(app, "HOLD", "/runs/cafebabe", &[], "").await;303320
match tenth.status() {304321
StatusCode::GONE => busted += 1,305322
StatusCode::REQUEST_TIMEOUT => {306323
assert_eq!(header(&tenth, "x-floor"), "4");307
- assert_eq!(308
- header(&tenth, "x-eq"),309
- "75",310
- "priapism holds the deflation at 75"311
- );324
+ assert_eq!(held.eq(), 75, "priapism holds the deflation at 75");312325
moved += 1;313326
}314327
other => panic!("seed {seed} overstayed into {other}"),@@ -345,15 +358,15 @@ async fn friction_at_the_limit_tears_the_foreskin() {
345358
346359
#[tokio::test]347360
async fn ice_treats_a_burn_even_through_priapism() {348
- let app = seeded_with(Floor::ChangingRoom, |run| {361
+ let held = harness(Floor::ChangingRoom, SEED, |run| {349362
run.body.eq = 80;350363
run.body.conditions.priapism_turns = 3;351364
run.body.conditions.friction_burn = true;352365
run.body.friction = 9;353366
run.items.stores.ice_packs = 1;354367
});355
- let iced = send(&app, "JELQ", "/runs/cafebabe?technique=ice", &[], "").await;356
- assert_eq!(header(&iced, "x-eq"), "80");368
+ let iced = send(&held.app, "JELQ", "/runs/cafebabe?technique=ice", &[], "").await;369
+ assert_eq!(held.eq(), 80);357370
assert_eq!(header(&iced, "x-friction"), "6/10");358371
assert!(warnings(&iced).iter().all(|w| !w.contains("friction burn")));359372
}@@ -424,3 +437,32 @@ async fn the_refractory_period_gives_back_some_stamina_and_rarely_all() {
424437
}425438
assert!((8..=35).contains(&full), "{full} second winds in 400");426439
}440
+441
+#[tokio::test]442
+async fn eq_is_named_rather_than_numbered() {443
+ for (eq, setup, named) in [444
+ (0, None, "Flaccid"),445
+ (20, None, "Cold"),446
+ (40, None, "Warm"),447
+ (60, None, "Ideal"),448
+ (60, Some("burn"), "Swollen"),449
+ (80, None, "Swollen"),450
+ (88, None, "Throbbing"),451
+ (85, Some("ring"), "Straining"),452
+ (93, None, "Straining"),453
+ (97, None, "Imminent"),454
+ (40, Some("priapism"), "Priapic"),455
+ ] {456
+ let app = seeded_with(Floor::Bedroom, |run| {457
+ run.body.eq = eq;458
+ match setup {459
+ Some("burn") => run.body.conditions.friction_burn = true,460
+ Some("ring") => run.items.kit.ring = true,461
+ Some("priapism") => run.body.conditions.priapism_turns = 3,462
+ _ => {}463
+ }464
+ });465
+ let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;466
+ assert_eq!(header(&view, "x-eq"), named, "{eq} with {setup:?}");467
+ }468
+}@@ -53,7 +53,7 @@ pre, li, p, td, label { overflow-wrap: anywhere; }
5353
const REL = "https://jelq.dev/rels/";5454
const METHODS = ["JELQ", "HOLD", "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"];5555
const ALL_LUBES = ["", "application/baby-oil", "application/coconut-oil", "application/lotion", "application/vaseline", "text/plain; charset=spit", "image/jpeg"];56
-const DEATHS = ["vascular-event", "caught", "opened", "banned", "degloved", "zip", "watermelon", "overstayed", "run-ended"];56
+const DEATHS = ["ruptured", "caught", "opened", "banned", "degloved", "zip", "watermelon", "overstayed", "run-ended"];5757
let stocked = null;5858
let excuses = [];5959
let nonce = null;@@ -64,7 +64,7 @@ const STATS = [
6464
["x-girth", "Girth"],6565
["x-foreskin", "Foreskin"],6666
["x-friction", "Friction"],67
- ["x-eq", "EQ (aim for 50 to 70)"],67
+ ["x-eq", "EQ"],6868
["x-stamina", "Stamina"],6969
["x-suspicion", "Suspicion"],7070
["x-shame", "Shame"],@@ -153,8 +153,8 @@ function options(name) {
153153
if (name === "technique") return techniques.map(t => t.name);154154
if (name === "also") {155155
const first = technique(fieldValue("technique"));156
- if (!first || !first.lubricant) return [""];157
- const partners = techniques.filter(t => t.lubricant && t.name !== first.name && !clashes(first, t));156
+ if (!first || !first.intensity) return [""];157
+ const partners = techniques.filter(t => t.intensity && t.name !== first.name && !clashes(first, t));158158
return [""].concat(partners.map(t => t.name));159159
}160160
if (name === "lube") {@@ -272,7 +272,7 @@ function fieldNames() {
272272
const chosen = technique(fieldValue("technique"));273273
names = ["technique"];274274
if (options("also").length > 1) names.push("also");275
- if (chosen && chosen.lubricant) names.push("intensity");275
+ if (chosen && chosen.intensity) names.push("intensity");276276
if (["required", "optional"].includes(lubeRule())) names.push("lube");277277
}278278
const scoped = run && !["runs", "global"].includes(t.key);@@ -330,7 +330,7 @@ function parseLinks(header) {
330330
const params = Object.fromEntries([...match[2].matchAll(/([a-z-]+)="([^"]*)"/g)].map(p => [p[1], p[2]]));331331
const name = (params.rel || "").replace(REL, "");332332
if (name === "technique") {333
- moves.push({ name: params.title, lubricant: params.lubricant || null });333
+ moves.push({ name: params.title, lubricant: params.lubricant || null, intensity: params.intensity || null });334334
continue;335335
}336336
const methods = (params.allow || "GET").split(",").map(s => s.trim());