diff --git a/README.md b/README.md index 04fec3d..65fbc86 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The data for fish is based on the data used in [Carbuncle Plushy's Fish Tracker] Current major missing features include: -- [ ] Fisher's Intution (catching other fish to spawn a big fish) +- [x] Fisher's Intuition (catching other fish to spawn a big fish) - [ ] Spearfishing - [x] Better condition display, show the weather, etc - [ ] Folklore requirement display diff --git a/src/changelog.rs b/src/changelog.rs index 2d76685..22ff6d3 100644 --- a/src/changelog.rs +++ b/src/changelog.rs @@ -6,6 +6,13 @@ pub fn changelog_page() -> Markup { layout(html! { h1 { "Beacon Changelog" } + section { + h2 { "1.4.0" } + ul { + li { "Implemented support for Fisher's Intuition" } + } + } + section { h2 { "1.3.0, 11.02.2025" } ul { diff --git a/src/data.rs b/src/data.rs index 2b776d7..7d8c119 100644 --- a/src/data.rs +++ b/src/data.rs @@ -189,6 +189,7 @@ pub struct CombinedFish<'a> { pub is_always_up: bool, pub windows: Vec, pub rarity: f32, + pub filtered: bool, } #[derive(Debug, Clone, Copy)] @@ -490,6 +491,7 @@ impl Data { let mut cfish = CombinedFish { entry: v, meta: m, + filtered: false, is_up: false, // fake default values for now is_always_up: false, // dito windows: Vec::new(), // dito @@ -528,9 +530,10 @@ impl Filters { &'a self, fish: Vec<&'a CombinedFish>, caught_fish_ids: &[i32], - ) -> Vec<&'a CombinedFish> { + ) -> Vec { fish.into_iter() - .filter(|fish| { + .cloned() + .map(|fish| { let f_caught = if self.include_caught { true } else { @@ -549,7 +552,10 @@ impl Filters { true }; - f_caught && f_patch && f_big + CombinedFish { + filtered: !(f_caught && f_patch && f_big), + ..fish + } }) .collect() } @@ -566,3 +572,8 @@ pub fn get_weather_icon(data: &Data, id: &u32) -> String { pub fn get_zone_name(data: &Data, id: u32) -> &str { &data.db_data.zones.get(&id).unwrap().name_en } + +pub fn display_intuition_length(length: u32) -> String { + let dur = Duration::seconds(length as i64); + format!("{}m{}s", dur.num_minutes(), dur.num_seconds() % 60) +} diff --git a/src/main.rs b/src/main.rs index 1305bc0..eb2b25d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,7 +85,7 @@ async fn main_handler( // Extract the list of fish we want to render. let meta = state.data.fish_with_meta(); - let mut values: Vec<&CombinedFish> = filters.filter(meta.values().collect(), &caught_fish); + let mut values: Vec = filters.filter(meta.values().collect(), &caught_fish); values.sort_by(|afish, bfish| { pinned_fish .contains(&(bfish.entry.id as i32)) diff --git a/src/templates.rs b/src/templates.rs index 1847c0a..695b853 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -11,7 +11,7 @@ use crate::{ pub struct ViewData<'a> { pub state: State>, - pub fish: Vec<&'a CombinedFish<'a>>, + pub fish: Vec>, pub caught_fish: Vec, pub pinned_fish: Vec, pub acc_id: String, @@ -75,129 +75,158 @@ pub fn main_page(data: ViewData) -> Markup { pub fn fish_list(data: &ViewData) -> Markup { html! { @for fish in data.fish.clone() { - section.fish.up[fish.is_up].alwaysup[fish.is_always_up].pinned[data.pinned_fish.contains(&(fish.entry.id as i32))] { - .title { - div { - @if !data.caught_fish.contains(&(fish.entry.id as i32)) { - form action=(format!("/{}/catch/{}", data.acc_id, fish.entry.id)) method="post" { - button.catch-button type="submit" { (PreEscaped("✓")) } + @if !fish.filtered { + (fish_display(&fish, data)) + @if !fish.entry.predators.is_empty() { + .predators { + div.predators-header { small { "⇖ Requirements" } } + @for predator in &fish.entry.predators { + @if let Some(pred_fish) = data.fish.iter().find(|f| f.entry.id == predator[0]) { + .predators-fish { + .amount { (predator[1]) } + (fish_display(pred_fish, data)) + } } } - - @if !data.pinned_fish.contains(&(fish.entry.id as i32)) { - form action=(format!("/{}/pin/{}", data.acc_id, fish.entry.id)) method="post" { - button.pin-button type="submit" { (PreEscaped("☆"))} - } - } @else { - form action=(format!("/{}/pin/{}/delete", data.acc_id, fish.entry.id)) method="post" { - button.pin-button type="submit" { (PreEscaped("★"))} - } - } - } - div { - h3 { - (fish.meta.name_en) - span class=(format!("patch patch-{}", fish.entry.patch as u32)) { (fish.entry.patch) } - } - .subtitle { - span { "Rarity: " (format!("{:.2}", fish.rarity * 100.)) "%" } - } } } - .when { - @if let Some(window) = fish.windows.first() { - @if fish.is_up { - "closes " (window.display_end_time()) - } @else { - "opens " (window.display_start_time()) - } - br; - @if fish.is_up { - .date data-ts=(clock::to_earth_time(window.start_time + window.duration).timestamp_millis()) { - .inner id=(format!("date-{}", fish.entry.id)) hx-preserve { - (clock::to_earth_time(window.start_time + window.duration).format("%c %Z")) - } - } + } + } + } +} - @if let Some(window2) = fish.windows.get(1) { - .date.tiny data-ts=(clock::to_earth_time(window2.start_time).timestamp_millis()) { - "next: " - .inner id=(format!("nextwindow-{}", fish.entry.id)) hx-preserve { - (clock::to_earth_time(window2.start_time).format("%c %Z")) - } - } - } - } @else { - .date data-ts=(clock::to_earth_time(window.start_time).timestamp_millis()) { - .inner id=(format!("date-{}", fish.entry.id)) hx-preserve { - (clock::to_earth_time(window.start_time).format("%c %Z")) - } - } +pub fn fish_display(fish: &CombinedFish, data: &ViewData) -> Markup { + html! { + section.fish.up[fish.is_up].alwaysup[fish.is_always_up].pinned[data.pinned_fish.contains(&(fish.entry.id as i32))] { + .title { + div { + @if !data.caught_fish.contains(&(fish.entry.id as i32)) { + form action=(format!("/{}/catch/{}", data.acc_id, fish.entry.id)) method="post" { + button.catch-button type="submit" { (PreEscaped("✓")) } } } + @if !data.pinned_fish.contains(&(fish.entry.id as i32)) { + form action=(format!("/{}/pin/{}", data.acc_id, fish.entry.id)) method="post" { + button.pin-button type="submit" { (PreEscaped("☆"))} + } + } @else { + form action=(format!("/{}/pin/{}/delete", data.acc_id, fish.entry.id)) method="post" { + button.pin-button type="submit" { (PreEscaped("★"))} + } + } } - .how { - @for item_id in &fish.entry.best_catch_path { - @if let Some(item) = data.state.data.db_data.items.get(item_id) { - span.catchpath title=(item.name_en) { - img src=(item.get_icon_url()) width="35"; + div.name { + h3 { + (fish.meta.name_en) + span class=(format!("patch patch-{}", fish.entry.patch as u32)) { (fish.entry.patch) } - @if let Some(hookset) = item.get_hookset(&data.state.data) { - img.hookset src=(hookset) width="20"; - } + .subtitle { + span { "Rarity: " (format!("{:.2}", fish.rarity * 100.)) "%" } + } + } - @if let Some(tug) = item.get_tug(&data.state.data) { - span.tug { (tug) } + @if let Some(length) = fish.entry.intuition_length { + .intuition { + img src="/static/intuition.png"; + div { (data::display_intuition_length(length)) } + } + } + } + } + .when { + @if let Some(window) = fish.windows.first() { + @if fish.is_up { + "closes " (window.display_end_time()) + } @else { + "opens " (window.display_start_time()) + } + br; + @if fish.is_up { + .date data-ts=(clock::to_earth_time(window.start_time + window.duration).timestamp_millis()) { + .inner id=(format!("date-{}", fish.entry.id)) hx-preserve { + (clock::to_earth_time(window.start_time + window.duration).format("%c %Z")) + } + } + + @if let Some(window2) = fish.windows.get(1) { + .date.tiny data-ts=(clock::to_earth_time(window2.start_time).timestamp_millis()) { + "next: " + .inner id=(format!("nextwindow-{}", fish.entry.id)) hx-preserve { + (clock::to_earth_time(window2.start_time).format("%c %Z")) } } } + } @else { + .date data-ts=(clock::to_earth_time(window.start_time).timestamp_millis()) { + .inner id=(format!("date-{}", fish.entry.id)) hx-preserve { + (clock::to_earth_time(window.start_time).format("%c %Z")) + } + } } - @if let Some(hookset) = fish.entry.hookset { - span.catchpath { - img src=(hookset) width="35"; + } - @if let Some(tug) = fish.entry.tug { + } + .how { + @for item_id in &fish.entry.best_catch_path { + @if let Some(item) = data.state.data.db_data.items.get(item_id) { + span.catchpath title=(item.name_en) { + img src=(item.get_icon_url()) width="35"; + + @if let Some(hookset) = item.get_hookset(&data.state.data) { + img.hookset src=(hookset) width="20"; + } + + @if let Some(tug) = item.get_tug(&data.state.data) { span.tug { (tug) } } } - } } - .meta { - @if let Some(location_id) = fish.entry.location { - @if let Some(location) = data.state.data.db_data.fishing_spots.get(&location_id) { - div { - span.zone { (fish.meta.zone_en) } - (location.name_en) - } + @if let Some(hookset) = fish.entry.hookset { + span.catchpath { + img src=(hookset) width="35"; + + @if let Some(tug) = fish.entry.tug { + span.tug { (tug) } } } - @if fish.entry.start_hour.is_some() && fish.entry.end_hour.is_some() { + } + } + .meta { + @if let Some(location_id) = fish.entry.location { + @if let Some(location) = data.state.data.db_data.fishing_spots.get(&location_id) { div { - @if !fish.is_always_up { - "ET " + span.zone { (fish.meta.zone_en) } + (location.name_en) + } + } + } + + @if fish.entry.start_hour.is_some() && fish.entry.end_hour.is_some() { + div { + @if !fish.is_always_up { + "ET " (clock::display_eorzea_time(&clock::set_hm_from_float(&clock::get_current_eorzea_date(), fish.entry.start_hour.unwrap()))) - "-" - (clock::display_eorzea_time(&clock::set_hm_from_float(&clock::get_current_eorzea_date(), fish.entry.end_hour.unwrap()))) + "-" + (clock::display_eorzea_time(&clock::set_hm_from_float(&clock::get_current_eorzea_date(), fish.entry.end_hour.unwrap()))) } @else { "always up!" - } } - div { - @if !fish.entry.weather_set.is_empty() { - @if !fish.entry.previous_weather_set.is_empty() { - @for weather in &fish.entry.previous_weather_set { - img src=(get_weather_icon(&data.state.data, weather)) width="20" title=(get_weather_name(&data.state.data, weather)); - } - - " ➞ " - } - - @for weather in &fish.entry.weather_set { + } + div { + @if !fish.entry.weather_set.is_empty() { + @if !fish.entry.previous_weather_set.is_empty() { + @for weather in &fish.entry.previous_weather_set { img src=(get_weather_icon(&data.state.data, weather)) width="20" title=(get_weather_name(&data.state.data, weather)); } + + " ➞ " + } + + @for weather in &fish.entry.weather_set { + img src=(get_weather_icon(&data.state.data, weather)) width="20" title=(get_weather_name(&data.state.data, weather)); } } } diff --git a/static/intuition.png b/static/intuition.png new file mode 100644 index 0000000..6e12759 Binary files /dev/null and b/static/intuition.png differ diff --git a/static/style.css b/static/style.css index 918d02f..1c5c857 100644 --- a/static/style.css +++ b/static/style.css @@ -46,6 +46,7 @@ select { .title .subtitle { margin: 0; font-size: 14px; + font-weight: normal; color: gray; } @@ -219,3 +220,51 @@ section.pinned { .pinned *:not(button) { color: white; } + +.pinned + .predators { + background: #ffd6cf; +} + +.predators-header { + color: gray; +} + +.predators-fish { + display: flex; + align-items: center; + gap: 5px; +} + +.predators { + margin-left: 30px; + padding: 0 10px; + margin-bottom: 10px; +} + +.predators-fish section { + flex-grow: 1; + border: 1px gray dotted; + border-radius: 5px; +} + +.predators-fish .amount { + font-size: 30px; + color: gray; +} + +.name { + display: flex; + align-items: center; + gap: 10px; +} + +.intuition { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.intuition div { + font-size: 12px; +}