an http based roguelike rpg about jelqing
Commit a11497af08aa
tarball11 files changed+72-52
@@ -89,10 +89,13 @@ impl Run {
8989
}9090
9191
pub fn techniques_now(&self) -> impl Iterator<Item = Technique> + '_ {92
- let blocked = !self.floor.timed() || self.items.kit.cage || self.body.exhausted;92
+ let blocked = !self.floor.timed() || self.items.kit.cage;9393
Technique::ALL9494
.into_iter()9595
.filter(move |_| !blocked)96
+ .filter(|&technique| {97
+ !self.body.exhausted || technique == Technique::Utility(Utility::Energy)98
+ })9699
.filter(|&technique| match technique {97100
Technique::Utility(Utility::Warmup) => {98101
!self.here.towel_warmed || self.here.kitchen.kettle_boiled@@ -14,7 +14,9 @@ impl Run {
1414
intensity: i32,1515
lube: Option<Lube>,1616
) -> Result<Outcome, Problem> {17
- self.check_exhausted()?;17
+ if techniques != [Technique::Utility(Utility::Energy)] {18
+ self.check_exhausted()?;19
+ }1820
if self.online.story == Story::Pending {1921
return Ok(self.branch_offer());2022
}@@ -23,7 +25,7 @@ impl Run {
2325
StatusCode::SEE_OTHER,2426
json!({ "detail": "Refer to the forum." }),2527
)26
- .header(header::LOCATION, super::FORUM_THREAD));28
+ .header(header::LOCATION, self.path("/thread")));2729
}2830
if self.items.kit.cage {2931
return Err(Problem::new(@@ -26,8 +26,16 @@ date,length,girth,technique,notes,honest?
2626
2009-06-14,7.50,5.50,wet,progress is slow but real,no2727
";2828
29
+fn thread(detail: Option<&str>) -> Outcome {30
+ let mut body = forum_thread();31
+ if let Some(detail) = detail {32
+ body["detail"] = json!(detail);33
+ }34
+ Outcome::json(StatusCode::OK, body).header(header::LAST_MODIFIED, ELDER_LAST_MODIFIED)35
+}36
+2937
fn nothing_new() -> Outcome {30
- Outcome::empty(StatusCode::NOT_MODIFIED).header(38
+ thread(None).header(3139
header::WARNING,3240
r#"199 - "Nothing new has been posted since 2009.""#,3341
)@@ -190,18 +198,18 @@ impl Run {
190198
return Err(Problem::new(191199
StatusCode::NOT_FOUND,192200
"no-thread",193
- "There is no thread to re-read. Nobody has mentioned a forum yet.",201
+ "There is no thread. Nobody has mentioned a forum yet.",194202
));195203
}196204
self.end_kitchen_visit();197205
if self.online.uli_taught {198
- return Ok(Outcome::json(StatusCode::OK, forum_thread()));206
+ return Ok(thread(None));199207
}200208
if self.here.thread_rolls >= THREAD_READS {201209
let age = SystemTime::now()202210
.duration_since(*ELDER_POSTED)203211
.map_or(0, |age| age.as_secs());204
- return Ok(Outcome::json(StatusCode::OK, forum_thread())212
+ return Ok(thread(None)205213
.header(header::AGE, age.to_string())206214
.header(header::WARNING, r#"112 - "Disconnected operation""#));207215
}@@ -216,7 +224,7 @@ impl Run {
216224
if !self.chance(217225
self.decision(218226
"teach-uli",219
- "Does this reread finally surface the 2009 reply that teaches uli?",227
+ "Does this read finally surface the 2009 reply that teaches uli?",220228
),221229
ULI_ODDS,222230
) {@@ -225,8 +233,7 @@ impl Run {
225233
self.online.uli_taught = true;226234
"Past the necroposting, a reply from TheElder lays out the technique. Uli. The full routine. You write it down before the tab can lose it."227235
};228
- Ok(Outcome::json(StatusCode::OK, json!({ "detail": detail }))229
- .header(header::LOCATION, self.path("/techniques")))236
+ Ok(thread(Some(detail)))230237
}231238
232239
fn learn_meditation(&mut self) -> bool {@@ -121,7 +121,7 @@ impl Run {
121121
}122122
Floor::Hotel => view["portal"] = json!(self.path("/portal")),123123
Floor::Forum => {124
- view["thread"] = json!(super::FORUM_THREAD);124
+ view["thread"] = json!(self.path("/thread"));125125
view["elder"] = json!(self.path("/elder"));126126
}127127
Floor::Clinic => view["diagnosis"] = json!(self.path("/diagnosis")),@@ -103,6 +103,7 @@ impl Run {
103103
}104104
self.items.stores.energy_drinks -= 1;105105
self.step_away();106
+ self.body.exhausted = false;106107
self.body.stamina = (self.body.stamina + ENERGY_STAMINA).min(self.max_stamina());107108
self.touch();108109
Ok(Outcome::json(@@ -1,7 +1,7 @@
11
use std::fmt::Write as _;22
3
-use super::{meta, run::RunResource, runs};4
-use crate::game::{Branch, FORUM_THREAD, Run, SHOP};3
+use super::{run::RunResource, runs};4
+use crate::game::{Branch, Run, SHOP};55
66
pub const REL_BASE: &str = "https://jelq.dev/rels/";77
@@ -43,7 +43,7 @@ pub fn header(run: &Run) -> String {
4343
("techniques", &runs::TECHNIQUES, true),4444
("drawer", &runs::DRAWER, reach.drawer),4545
("kitchen", &runs::KITCHEN, reach.kitchen),46
- ("re-read", &runs::REREAD, reach.thread),46
+ ("thread", &runs::THREAD, reach.thread),4747
("grip", &runs::GRIP, true),4848
("shop", &runs::SHOP, reach.shop),4949
("portal", &runs::PORTAL, reach.portal),@@ -66,16 +66,6 @@ pub fn header(run: &Run) -> String {
6666
}6767
.write(&mut out);6868
}69
- if reach.thread {70
- Link {71
- href: FORUM_THREAD,72
- rel: "thread",73
- allow: &meta::THREAD.allow_any(),74
- title: None,75
- lubricant: None,76
- }77
- .write(&mut out);78
- }7969
if reach.branch_pending {8070
for (branch, title) in [8171
(Branch::Forum, "Read the forum"),@@ -146,7 +146,7 @@ fn api(state: Shared) -> Router {
146146
.route("/runs/{id}/branches/{choice}", scoped(&runs::BRANCH))147147
.route("/runs/{id}/kitchen", scoped(&runs::KITCHEN))148148
.route("/runs/{id}/kitchen/watermelon", scoped(&runs::MELON))149
- .route("/runs/{id}/re-read", scoped(&runs::REREAD))149
+ .route("/runs/{id}/thread", scoped(&runs::THREAD))150150
.route("/runs/{id}/floor", scoped(&runs::FLOOR))151151
.route("/runs/{id}/shop", scoped(&runs::SHOP))152152
.route("/runs/{id}/shop/{item}", scoped(&runs::BUY))@@ -84,11 +84,6 @@ impl<T> Resource<T> {
8484
Self::listed(self.verbs.iter().filter(|verb| verb.available(target)))8585
}8686
87
- /// The methods with every guard assumed open, for resources that have none.88
- pub fn allow_any(&self) -> String {89
- Self::listed(self.verbs.iter())90
- }91
-9287
fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String {9388
let mut methods = Vec::new();9489
for verb in verbs {@@ -71,7 +71,7 @@ pub static DRAWER: RunResource = home(&[Verb::new("POST", search_drawer)]);
7171
pub static BRANCH: RunResource = home(&[Verb::new("POST", choose_branch)]);7272
pub static KITCHEN: RunResource = home(&[Verb::new("POST", raid_kitchen)]);7373
pub static MELON: RunResource = home(&[Verb::new("POST", commit_melon)]);74
-pub static REREAD: RunResource = pockets(&[Verb::new("POST", re_read)]);74
+pub static THREAD: RunResource = pockets(&[Verb::new("GET", get_thread)]);7575
pub static FLOOR: RunResource =7676
home(&[Verb::new("GET", get_floor), Verb::new("POST", leave_floor)]);7777
pub static SHOP: RunResource = home(&[Verb::new("GET", get_shop)]);@@ -229,7 +229,7 @@ fn commit_melon(run: &mut Run, _: &Ctx) -> Response {
229229
respond(run.commit_melon())230230
}231231
232
-fn re_read(run: &mut Run, _: &Ctx) -> Response {232
+fn get_thread(run: &mut Run, _: &Ctx) -> Response {233233
respond(run.read_thread())234234
}235235
@@ -169,7 +169,7 @@ async fn the_thread_announces_itself_at_the_branch() {
169169
)170170
.await;171171
let ninth = send(&app, "GET", "/runs/cafebabe", &[], "").await;172
- assert!(!header(&ninth, "link").contains("/forum/threads/2009"));172
+ assert!(!header(&ninth, "link").contains("/cafebabe/thread>"));173173
let tenth = send(174174
&app,175175
"JELQ",@@ -185,13 +185,13 @@ async fn the_thread_announces_itself_at_the_branch() {
185185
.iter()186186
.any(|w| w.to_str().unwrap().contains("a forum thread from 2009"))187187
);188
- assert!(header(&tenth, "link").contains("/forum/threads/2009"));188
+ assert!(header(&tenth, "link").contains("/cafebabe/thread>"));189189
let announced = send(&app, "GET", "/runs/cafebabe", &[], "").await;190
- assert!(header(&announced, "link").contains("/forum/threads/2009"));190
+ assert!(header(&announced, "link").contains("/cafebabe/thread>"));191191
}192192
193193
#[tokio::test]194
-async fn rereading_the_thread_teaches_uli() {194
+async fn reading_the_thread_teaches_uli() {195195
let mut taught_runs = 0;196196
for seed in 0..100u64 {197197
let app = app_with(Floor::Bedroom, seed, |run| {@@ -202,18 +202,17 @@ async fn rereading_the_thread_teaches_uli() {
202202
});203203
let mut taught = false;204204
for _ in 0..2 {205
- let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;206
- match read.status() {207
- StatusCode::NOT_MODIFIED => {208
- assert!(header(&read, "warning").contains("2009"));209
- }210
- StatusCode::OK => {211
- assert!(header(&read, "location").contains("/techniques"));212
- taught = true;213
- break;214
- }215
- other => panic!("seed {seed} read the thread into {other}"),205
+ let read = send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;206
+ assert_eq!(read.status(), StatusCode::OK, "seed {seed}");207
+ let warned = warnings(&read).iter().any(|w| w.contains("2009"));208
+ let body = json(read).await;209
+ assert!(body["posts"].is_array(), "a read always shows the thread");210
+ if body["detail"].is_string() {211
+ assert!(!warned);212
+ taught = true;213
+ break;216214
}215
+ assert!(warned);217216
}218217
let state = send(&app, "GET", "/runs/cafebabe", &[], "").await;219218
assert_eq!(header(&state, "x-stamina"), "10/10", "reading is free");@@ -222,7 +221,7 @@ async fn rereading_the_thread_teaches_uli() {
222221
let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=uli", &[], "").await;223222
assert_ne!(jelq.status(), StatusCode::FORBIDDEN);224223
} else {225
- let cached = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;224
+ let cached = send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;226225
assert_eq!(cached.status(), StatusCode::OK);227226
assert!(header(&cached, "warning").starts_with("112"));228227
assert!(header(&cached, "age").parse::<u64>().unwrap() > 0);@@ -235,12 +234,12 @@ async fn rereading_the_thread_teaches_uli() {
235234
}236235
237236
#[tokio::test]238
-async fn the_thread_cannot_be_reread_before_anyone_mentions_it() {237
+async fn the_thread_cannot_be_read_before_anyone_mentions_it() {239238
let app = seeded_with(Floor::Bedroom, |run| run.body.warmed_up = true);240
- let read = send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;239
+ let read = send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;241240
assert_eq!(read.status(), StatusCode::NOT_FOUND);242241
let after = send(&app, "GET", "/runs/cafebabe", &[], "").await;243
- assert!(!header(&after, "link").contains("/re-read"));242
+ assert!(!header(&after, "link").contains("/thread"));244243
}245244
246245
#[tokio::test]@@ -475,7 +474,7 @@ async fn the_forum_teaches_breathing_before_uli() {
475474
assert_eq!(meditate["locked"], true);476475
send(&app, "POST", "/runs/cafebabe/branches/forum", &[], "").await;477476
for _ in 0..2 {478
- send(&app, "POST", "/runs/cafebabe/re-read", &[], "").await;477
+ send(&app, "GET", "/runs/cafebabe/thread", &[], "").await;479478
}480479
let meditated = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;481480
if meditated.status() == StatusCode::OK {@@ -251,3 +251,26 @@ async fn queued_gains_announce_their_arrival() {
251251
let later = send(&app, "GET", "/runs/cafebabe", &[], "").await;252252
assert!(later.headers().get("x-gains-arrived").is_none());253253
}254
+255
+#[tokio::test]256
+async fn an_energy_drink_ends_exhaustion_without_the_refractory_period() {257
+ let app = seeded_with(Floor::Bedroom, |run| {258
+ run.body.warmed_up = true;259
+ run.body.eq = 60;260
+ run.body.stamina = 0;261
+ run.body.exhausted = true;262
+ run.items.stores.energy_drinks = 1;263
+ });264
+ let tired = send(&app, "GET", "/runs/cafebabe", &[], "").await;265
+ assert_eq!(advertised(&tired), ["energy"]);266
+ let drink = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;267
+ assert_eq!(drink.status(), StatusCode::OK);268
+ assert_eq!(header(&drink, "x-stamina"), "3/10");269
+ assert_eq!(270
+ header(&drink, "x-eq"),271
+ "60",272
+ "still warm, unlike the refractory HOLD"273
+ );274
+ let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;275
+ assert_ne!(jelq.status(), StatusCode::TOO_MANY_REQUESTS);276
+}