an http based roguelike rpg about jelqing
Commit 0fdc47364c8c
tarball8 files changed+152-22
@@ -58,6 +58,8 @@ pub const PULLED_MUSCLE: u32 = 12;
5858
pub const PULLED_MUSCLE_INTENSITY: i32 = 9;5959
pub const RING_PRIAPISM: u32 = 10;6060
pub const SUPPLEMENT_WORKS: u32 = 5;61
+pub const MEDITATION_ODDS: u32 = 2;62
+pub const MEDITATION_EQ: i32 = 10;6163
pub const OBSERVED: u32 = 6;6264
pub const CAT_TAKES_TOWEL: u32 = 5;6365
pub const FRIEND: u32 = 10;@@ -3,7 +3,7 @@ use serde_json::{Value, json};
33
44
use super::{55
Cause, Elder, Floor, Outcome, Run, Story,6
- balance::{EXAGGERATION, PATIENCE},6
+ balance::{EXAGGERATION, MEDITATION_ODDS, PATIENCE},77
body::exaggeration,88
honest, parse_credentials,99
};@@ -27,6 +27,9 @@ pub fn forum_thread() -> Value {
2727
"posts": [2828
{ "author": "TheElder", "body": "Warm up. Always warm up. A cold jelq is a 412 waiting to happen." },2929
{ "author": "TheElder", "body": "Keep EQ between 50 and 70. Above 70 you are gambling. Above 90 you are a cautionary tale." },30
+ { "author": "newbie_2009", "body": "everyone keeps saying uli. who is uli" },31
+ { "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." },32
+ { "author": "TheElder", "body": "We do not say his name in vain. We say it in girth." },3033
{ "author": "newbie_2009", "body": "I have been sending PUT for three weeks and nothing has changed." },3134
{ "author": "TheElder", "body": "PUT is idempotent. Read the spec." },3235
{ "author": "TheElder", "body": "Members verify with a Ruler. Proxy-Authorization, inches, no rounding up. We can tell." },@@ -78,11 +81,14 @@ impl Run {
7881
let (story, location, detail) = match choice {7982
"forum" => {8083
self.shame += 3;81
- (82
- Story::ReadForum,83
- self.path("/techniques"),84
- "The thread is from 2009. A technique called uli is discussed at length.",85
- )84
+ let taught = self.chance(MEDITATION_ODDS);85
+ self.meditation_taught |= taught;86
+ let detail = if taught {87
+ "The thread is from 2009. A technique called uli is discussed at length. Among the replies, a post teaches a way of breathing that lowers EQ."88
+ } else {89
+ "The thread is from 2009. A technique called uli is discussed at length."90
+ };91
+ (Story::ReadForum, self.path("/techniques"), detail)8692
}8793
"close-tab" => {8894
self.suspicion = 0;@@ -63,6 +63,7 @@ pub struct Run {
6363
pub warmed_up: bool,6464
pub put_applied: bool,6565
pub story: Story,66
+ pub meditation_taught: bool,6667
pub mum: Option<MumVisit>,6768
pub heard: Vec<String>,6869
pub elder: Elder,@@ -148,6 +149,7 @@ impl Run {
148149
warmed_up: false,149150
put_applied: false,150151
story: Story::Unopened,152
+ meditation_taught: false,151153
mum: None,152154
heard: Vec::new(),153155
notices: Vec::new(),@@ -138,6 +138,7 @@ pub struct FloorState {
138138
pub towel_stolen: bool,139139
pub brother_told: bool,140140
pub shopping: bool,141
+ pub meditated: bool,141142
pub wifi_accepted: bool,142143
}143144
@@ -77,6 +77,7 @@ pub enum Utility {
7777
Edge,7878
Pump,7979
Supplement,80
+ Meditate,8081
}8182
8283
#[derive(Clone, Copy, PartialEq, Eq, Debug)]@@ -168,6 +169,7 @@ impl Utility {
168169
Self::Edge => "edge",169170
Self::Pump => "pump",170171
Self::Supplement => "supplement",172
+ Self::Meditate => "meditate",171173
}172174
}173175
@@ -178,17 +180,19 @@ impl Utility {
178180
Self::Edge => "EQ +15 and a little girth. Three in a row is a loop.",179181
Self::Pump => "Large gains, all temporary. Requires a pump.",180182
Self::Supplement => "Consumes a supplement.",183
+ Self::Meditate => "EQ -10. Taught by the forum. Once per floor, no stamina.",181184
}182185
}183186
}184187
185188
impl Technique {186
- pub const ALL: [Self; 9] = [189
+ pub const ALL: [Self; 10] = [187190
Self::Utility(Utility::Warmup),188191
Self::Utility(Utility::Ice),189192
Self::Utility(Utility::Edge),190193
Self::Utility(Utility::Pump),191194
Self::Utility(Utility::Supplement),195
+ Self::Utility(Utility::Meditate),192196
Self::Training(Training::Wet),193197
Self::Training(Training::Dry),194198
Self::Training(Training::Stretch),@@ -8,9 +8,9 @@ use super::{
88
Floor, Grip, LUBRICANTS, Lube, Outcome, Payload, Run, Story, Technique,99
balance::{1010
BAND_BONUS, BRANCH_TURN, CAT_TAKES_TOWEL, CHANGING_ROOM_CHILL, DANGER_EQ, EQ_BAND,11
- EQ_WOBBLE, EXTENDER_GAIN, HOT_EQ, OBSERVED, PRIAPISM_EQ, PRIAPISM_TURNS, PULLED_MUSCLE,12
- PULLED_MUSCLE_INTENSITY, RING_PRIAPISM, RING_RUPTURE_LINE, RUPTURE_LINE, STORAGE_GIRTH,13
- SUSPICION_LIMIT,11
+ EQ_WOBBLE, EXTENDER_GAIN, HOT_EQ, MEDITATION_EQ, OBSERVED, PRIAPISM_EQ, PRIAPISM_TURNS,12
+ PULLED_MUSCLE, PULLED_MUSCLE_INTENSITY, RING_PRIAPISM, RING_RUPTURE_LINE, RUPTURE_LINE,13
+ STORAGE_GIRTH, SUSPICION_LIMIT,1414
},1515
not_extended, round3,1616
state::{MumVisit, Observer, Pending},@@ -44,6 +44,7 @@ impl Run {
4444
"The cage is locked. The key is not in the box.",4545
));4646
}47
+ self.here.shopping = false;4748
match *techniques {4849
[Technique::Utility(utility)] => self.utility(utility, lube, now),4950
[Technique::Training(training)] => self.single(training, intensity, lube, now),@@ -73,6 +74,7 @@ impl Run {
7374
Utility::Warmup => return self.warm_up(),7475
Utility::Ice => return self.ice(),7576
Utility::Supplement => return self.supplement(),77
+ Utility::Meditate => return self.meditate(),7678
Utility::Edge | Utility::Pump => {}7779
}7880
if utility == Utility::Pump {@@ -189,9 +191,20 @@ impl Run {
189191
return Err(Problem::new(190192
StatusCode::FORBIDDEN,191193
"technique-locked",192
- "This technique is discussed on the forum. You have not been to the forum.",194
+ "uli is discussed on the forum, in a thread from 2009, by a man who never posted again. You have not been to the forum.",193195
));194196
}197
+ if technique == Technique::Utility(Utility::Meditate) {198
+ return if self.meditation_taught {199
+ Ok(())200
+ } else {201
+ Err(Problem::new(202
+ StatusCode::FORBIDDEN,203
+ "technique-locked",204
+ "The breathing technique is described in one of the forum's replies. You have not found it.",205
+ ))206
+ };207
+ }195208
if !self.warmed_up {196209
return Err(Problem::new(197210
StatusCode::PRECONDITION_FAILED,@@ -251,6 +264,9 @@ impl Run {
251264
Technique::Utility(Utility::Warmup) => true,252265
Technique::Utility(Utility::Ice) => self.items.ice_packs > 0,253266
Technique::Utility(Utility::Supplement) => self.items.supplements > 0,267
+ Technique::Utility(Utility::Meditate) => {268
+ self.meditation_taught && !self.here.meditated269
+ }254270
_ => self.access(technique).is_ok() && self.check_storage().is_ok(),255271
})256272
.collect()@@ -307,7 +323,6 @@ impl Run {
307323
self.touch();308324
self.turn += 1;309325
self.here.turns += 1;310
- self.here.shopping = false;311326
self.realise();312327
if self.items.extender {313328
self.length += EXTENDER_GAIN;@@ -362,9 +377,33 @@ impl Run {
362377
}363378
if self.turn == BRANCH_TURN && self.story == Story::Unopened {364379
self.story = Story::Pending;380
+ self.notices381
+ .push("Somewhere, a forum thread from 2009 still has replies.");365382
}366383
}367384
385
+ /// The breathing technique from the forum's replies. It spends no stamina and the turn is386
+ /// not the floor's, but each floor allows it once.387
+ fn meditate(&mut self) -> Result<Outcome, Problem> {388
+ self.access(Technique::Utility(Utility::Meditate))?;389
+ if self.here.meditated {390
+ return Err(Problem::new(391
+ StatusCode::CONFLICT,392
+ "already-meditated",393
+ "This floor has had its meditation.",394
+ ));395
+ }396
+ self.here.meditated = true;397
+ self.eq = (self.eq - MEDITATION_EQ).clamp(0, 100);398
+ self.touch();399
+ Ok(Outcome::json(400
+ StatusCode::OK,401
+ json!({402
+ "detail": "In through the nose, out through the mouth. EQ falls by ten.",403
+ }),404
+ ))405
+ }406
+368407
pub(super) fn maybe_summon_mum(&mut self, was_below: bool) {369408
if !self.floor.has_mum() || self.mum.is_some() {370409
return;@@ -11,7 +11,10 @@ use tokio::io::AsyncWriteExt;
1111
1212
use super::{AppState, ctx::Ctx, meta, middleware::Compress, resource::Resource, runs};1313
use crate::{14
- game::{Elder, FORUM_THREAD, Floor, Run, SHOP, Story, balance::SUSPICION_LIMIT},14
+ game::{15
+ Elder, FORUM_THREAD, Floor, Run, SHOP, Story,16
+ balance::{BRANCH_TURN, SUSPICION_LIMIT},17
+ },1518
headers::{1619
X_CASH, X_EQ, X_FLOOR, X_FORESKIN, X_GIRTH, X_LENGTH, X_OBSERVED_BY,1720
X_SELF_REPORTED_LENGTH, X_SHAME, X_STAMINA, X_SUSPICION,@@ -129,14 +132,15 @@ fn links(run: &Run, now: Instant) -> String {
129132
let allow = target.resource.allow(run);130133
format!("<{path}>; rel=\"{REL_BASE}{name}\"; allow=\"{allow}\"")131134
})132
- .chain(133
- (run.forum_read() || run.floor.number() >= Floor::Forum.number()).then(|| {134
- format!(135
- "<{FORUM_THREAD}>; rel=\"{REL_BASE}thread\"; allow=\"{}\"",136
- meta::THREAD.allow_any()137
- )138
- }),139
- )135
+ .chain((run.turn >= BRANCH_TURN136
+ || run.forum_read()137
+ || run.floor.number() >= Floor::Forum.number())138
+ .then(|| {139
+ format!(140
+ "<{FORUM_THREAD}>; rel=\"{REL_BASE}thread\"; allow=\"{}\"",141
+ meta::THREAD.allow_any()142
+ )143
+ }),)140144
.chain((run.story == Story::Pending).then(|| {141145
let choice = |slug: &str, title: &str| {142146
format!(@@ -11,7 +11,7 @@ use tower::ServiceExt;
1111
1212
use crate::{1313
AppState, Config,14
- game::{Floor, MumVisit, Run, Story, balance::SUSPICION_LIMIT},14
+ game::{Floor, MumVisit, Run, Story, balance::BRANCH_TURN, balance::SUSPICION_LIMIT},1515
router,1616
};1717
@@ -946,3 +946,75 @@ async fn shopping_survives_browsing_but_not_jelqing() {
946946
let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;947947
assert!(!header(&after, "link").contains("shop-item"));948948
}949
+950
+#[tokio::test]951
+async fn the_thread_announces_itself_at_the_branch() {952
+ let app = seeded_with(Floor::Bathroom, |run| {953
+ run.turn = 8;954
+ run.warmed_up = true;955
+ });956
+ send(957
+ &app,958
+ "JELQ",959
+ "/runs/cafebabe?technique=wet",960
+ &[("content-type", "application/baby-oil")],961
+ "",962
+ )963
+ .await;964
+ let ninth = send(&app, "GET", "/runs/cafebabe", &[], "").await;965
+ assert!(!header(&ninth, "link").contains("/forum/threads/2009"));966
+ let tenth = send(967
+ &app,968
+ "JELQ",969
+ "/runs/cafebabe?technique=wet",970
+ &[("content-type", "application/baby-oil")],971
+ "",972
+ )973
+ .await;974
+ assert!(975
+ tenth976
+ .headers()977
+ .get_all("warning")978
+ .iter()979
+ .any(|w| w.to_str().unwrap().contains("a forum thread from 2009"))980
+ );981
+ assert!(header(&tenth, "link").contains("/forum/threads/2009"));982
+ let announced = send(&app, "GET", "/runs/cafebabe", &[], "").await;983
+ assert!(header(&announced, "link").contains("/forum/threads/2009"));984
+}985
+986
+#[tokio::test]987
+async fn reading_the_forum_may_teach_meditation() {988
+ let taught = (0..50u64)989
+ .filter(|seed| {990
+ let mut run = Run::new("cafebabe".into(), "cafe".into(), *seed);991
+ run.story = Story::Pending;992
+ run.turn = BRANCH_TURN;993
+ run.choose("forum").is_ok() && run.meditation_taught994
+ })995
+ .count();996
+ assert!(997
+ taught > 0 && taught < 50,998
+ "the teach roll should be a coin, it landed {taught}/50"999
+ );1000
+}1001
+1002
+#[tokio::test]1003
+async fn meditation_is_free_but_only_once_per_floor() {1004
+ let app = seeded_with(Floor::Bedroom, |run| run.meditation_taught = true);1005
+ let response = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;1006
+ assert_eq!(response.status(), StatusCode::OK);1007
+ assert_eq!(header(&response, "x-eq"), "10");1008
+ assert_eq!(header(&response, "x-stamina"), "10/10");1009
+ let again = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;1010
+ assert_eq!(again.status(), StatusCode::CONFLICT);1011
+}1012
+1013
+#[tokio::test]1014
+async fn meditation_stays_hidden_until_the_forum_teaches_it() {1015
+ let app = seeded_with(Floor::Bedroom, |_| {});1016
+ let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;1017
+ assert!(!header(&view, "link").contains("technique=meditate"));1018
+ let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;1019
+ assert_eq!(jelq.status(), StatusCode::FORBIDDEN);1020
+}