an http based roguelike rpg about jelqing
Commit d604371299fb
tarball5 files changed+95-18
@@ -190,4 +190,9 @@ impl Run {
190190
)191191
.header(header::LOCATION, self.path("")))192192
}193
+194
+ /// Whether searching the drawer on this floor would turn anything up.195
+ pub fn drawer_worth_searching(&self) -> bool {196
+ self.here.drawer_searches == 0 || (self.floor == Floor::Nans && self.items.cage)197
+ }193198
}@@ -58,8 +58,17 @@ impl<T> Resource<T> {
5858
}5959
6060
pub fn allow(&self, target: &T) -> String {61
+ Self::listed(self.verbs.iter().filter(|verb| verb.available(target)))62
+ }63
+64
+ /// The methods with every guard assumed open, for resources that have none.65
+ pub fn allow_any(&self) -> String {66
+ Self::listed(self.verbs.iter())67
+ }68
+69
+ fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String {6170
let mut methods = Vec::new();62
- for verb in self.verbs.iter().filter(|verb| verb.available(target)) {71
+ for verb in verbs {6372
methods.push(verb.method);6473
if verb.method == "GET" {6574
methods.push("HEAD");@@ -9,9 +9,9 @@ use axum::{
99
use hyper_util::rt::TokioIo;1010
use tokio::io::AsyncWriteExt;1111
12
-use super::{AppState, ctx::Ctx, middleware::Compress, resource::Resource, runs};12
+use super::{AppState, ctx::Ctx, meta, middleware::Compress, resource::Resource, runs};1313
use crate::{14
- game::{Floor, Run, balance::SUSPICION_LIMIT},14
+ game::{Elder, FORUM_THREAD, Floor, Run, balance::SUSPICION_LIMIT},1515
headers::{1616
X_CASH, X_EQ, X_FLOOR, X_FORESKIN, X_GIRTH, X_OBSERVED_BY, X_SELF_REPORTED_LENGTH, X_SHAME,1717
X_STAMINA, X_SUSPICION,@@ -101,13 +101,21 @@ fn links(run: &Run, now: Instant) -> String {
101101
("foreskin", &runs::FORESKIN, run.foreskin.is_some()),102102
("inventory", &runs::INVENTORY, true),103103
("techniques", &runs::TECHNIQUES, true),104
- ("drawer", &runs::DRAWER, true),104
+ ("drawer", &runs::DRAWER, run.drawer_worth_searching()),105105
("grip", &runs::GRIP, true),106106
("shop", &runs::SHOP, run.floor.shop_open()),107
- ("portal", &runs::PORTAL, run.floor == Floor::Hotel),108
- ("elder", &runs::ELDER, run.floor == Floor::Forum),107
+ (108
+ "portal",109
+ &runs::PORTAL,110
+ run.floor == Floor::Hotel && !run.here.wifi_accepted,111
+ ),112
+ (113
+ "elder",114
+ &runs::ELDER,115
+ run.floor == Floor::Forum && run.elder != Elder::Beaten,116
+ ),109117
("footage", &runs::FOOTAGE, run.filmed_at.is_some()),110
- ("diagnosis", &runs::DIAGNOSIS, true),118
+ ("diagnosis", &runs::DIAGNOSIS, run.floor == Floor::Clinic),111119
];112120
linked113121
.into_iter()@@ -121,6 +129,14 @@ fn links(run: &Run, now: Instant) -> String {
121129
let allow = target.resource.allow(run);122130
format!("<{path}>; rel=\"{REL_BASE}{name}\"; allow=\"{allow}\"")123131
})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
+ )124140
.chain(run.techniques_now(now).into_iter().map(|technique| {125141
let name = technique.name();126142
let lubricant = technique@@ -749,3 +749,33 @@ async fn links_offer_only_usable_techniques() {
749749
let response = send(&forum, "GET", "/runs/cafebabe", &[], "").await;750750
assert!(advertised(&response).is_empty());751751
}752
+753
+fn linked_rels(response: &Response) -> Vec<String> {754
+ header(response, "link")755
+ .split(", <")756
+ .filter_map(|entry| {757
+ entry758
+ .split("rels/")759
+ .nth(1)?760
+ .split('"')761
+ .next()762
+ .map(str::to_owned)763
+ })764
+ .filter(|rel| rel != "technique")765
+ .collect()766
+}767
+768
+#[tokio::test]769
+async fn links_hide_resources_that_would_do_nothing() {770
+ let (bedroom, _) = seeded(Floor::Bedroom);771
+ let before = linked_rels(&send(&bedroom, "GET", "/runs/cafebabe", &[], "").await);772
+ assert!(before.contains(&"drawer".to_owned()));773
+ assert!(!before.contains(&"diagnosis".to_owned()));774
+ assert!(!before.contains(&"thread".to_owned()));775
+ let searched = send(&bedroom, "POST", "/runs/cafebabe/drawer", &[], "").await;776
+ assert!(!linked_rels(&searched).contains(&"drawer".to_owned()));777
+778
+ let (clinic, _) = seeded(Floor::Clinic);779
+ let rels = linked_rels(&send(&clinic, "GET", "/runs/cafebabe", &[], "").await);780
+ assert!(rels.contains(&"diagnosis".to_owned()) && rels.contains(&"thread".to_owned()));781
+}@@ -91,11 +91,19 @@ const BY_TARGET = {
9191
"grip GET": ["upgrade"],9292
"diagnosis GET": ["authorization", "ifNoneMatch"],9393
};94
+const GROUPS = [95
+ ["play", "Play"],96
+ ["you", "You"],97
+ ["said", "From the last response"],98
+ ["reading", "Reading"],99
+ ["meta", "Meta"],100
+];101
+const GROUP_OF = { run: "play", floor: "play", drawer: "play", shop: "play", portal: "play", elder: "play", diagnosis: "play", length: "you", foreskin: "you", grip: "you", inventory: "you", footage: "you", techniques: "reading", thread: "reading" };102
+const NAMES = { run: "the run", thread: "the 2009 forum thread" };94103
const GLOBALS = [95
- { key: "runs", name: "start a new run", path: "/runs", methods: ["POST"] },96
- { key: "global", name: "leaderboard", path: "/leaderboard", methods: ["GET", "HEAD", "OPTIONS"] },97
- { key: "global", name: "graveyard", path: "/graveyard", methods: ["GET", "HEAD", "OPTIONS"] },98
- { key: "global", name: "the 2009 forum thread", path: "/forum/threads/2009", methods: ["GET", "HEAD", "OPTIONS"] },104
+ { key: "runs", group: "play", name: "start a new run", path: "/runs", methods: ["POST"] },105
+ { key: "global", group: "meta", name: "leaderboard", path: "/leaderboard", methods: ["GET", "HEAD", "OPTIONS"] },106
+ { key: "global", group: "meta", name: "graveyard", path: "/graveyard", methods: ["GET", "HEAD", "OPTIONS"] },99107
];100108
101109
let run = null;@@ -170,7 +178,10 @@ function allowedMethods(t) {
170178
}171179
172180
function targets() {173
- return (run ? linked : []).concat(offered, GLOBALS);181
+ const listed = run ? linked : [];182
+ const known = new Set(listed.concat(GLOBALS).map(t => t.path));183
+ const fresh = offered.filter((t, i) => !known.has(t.path) && offered.findIndex(o => o.path === t.path) === i);184
+ return listed.concat(fresh, GLOBALS);174185
}175186
176187
function current() {@@ -217,9 +228,15 @@ function renderAlerts(list) {
217228
function renderTargets() {218229
const box = $("targets");219230
box.replaceChildren();220
- for (const t of targets()) {221
- const radio = el("input", { type: "radio", name: "target", checked: t.path === target, onchange: () => chooseTarget(t) });222
- box.append(el("label", {}, radio, " " + t.name + " ", el("small", { textContent: short(t.path) })), el("br"));231
+ const all = targets();232
+ for (const [group, title] of GROUPS) {233
+ const members = all.filter(t => t.group === group);234
+ if (!members.length) continue;235
+ box.append(el("h4", { textContent: title }));236
+ for (const t of members) {237
+ const radio = el("input", { type: "radio", name: "target", checked: t.path === target, onchange: () => chooseTarget(t) });238
+ box.append(el("label", {}, radio, " " + t.name + " ", el("small", { textContent: short(t.path) })), el("br"));239
+ }223240
}224241
}225242
@@ -295,7 +312,7 @@ function parseLinks(header) {
295312
continue;296313
}297314
const methods = (params.allow || "GET").split(",").map(s => s.trim());298
- resources.push({ key: name, name: name === "run" ? "the run" : name, path: match[1], methods });315
+ resources.push({ key: name, group: GROUP_OF[name] || "play", name: NAMES[name] || name, path: match[1], methods });299316
}300317
return [resources, moves];301318
}@@ -304,7 +321,7 @@ function hrefs(value, found) {
304321
if (value && typeof value === "object") {305322
if (typeof value.href === "string") {306323
const name = value.title || value.item || value.name || value.href.split("/").pop();307
- found.push({ key: "offered", name, path: value.href, methods: [value.method || "GET"] });324
+ found.push({ key: "offered", group: "said", name, path: value.href, methods: [value.method || "GET"] });308325
}309326
for (const key in value) hrefs(value[key], found);310327
}@@ -356,7 +373,7 @@ function absorb(r, sentMethod, sentPath) {
356373
if (headers.link) [linked, techniques] = parseLinks(headers.link);357374
offered = body ? hrefs(body, []) : [];358375
if (headers.location && !started) {359
- offered.unshift({ key: "offered", name: "Location", path: headers.location, methods: ["GET"] });376
+ offered.unshift({ key: "offered", group: "said", name: short(headers.location).split("/").pop() + " (Location)", path: headers.location, methods: ["GET"] });360377
}361378
const abandoned = sentMethod === "DELETE" && sentPath === run && r.status === 204;362379
if (run && (abandoned || DEATHS.includes(problem))) {