support fisher's intuition

This commit is contained in:
insects 2025-02-11 15:45:57 +01:00
parent b966d78be3
commit a99ee46d26
7 changed files with 194 additions and 98 deletions

View file

@ -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: 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 - [ ] Spearfishing
- [x] Better condition display, show the weather, etc - [x] Better condition display, show the weather, etc
- [ ] Folklore requirement display - [ ] Folklore requirement display

View file

@ -6,6 +6,13 @@ pub fn changelog_page() -> Markup {
layout(html! { layout(html! {
h1 { "Beacon Changelog" } h1 { "Beacon Changelog" }
section {
h2 { "1.4.0" }
ul {
li { "Implemented support for Fisher's Intuition" }
}
}
section { section {
h2 { "1.3.0, 11.02.2025" } h2 { "1.3.0, 11.02.2025" }
ul { ul {

View file

@ -189,6 +189,7 @@ pub struct CombinedFish<'a> {
pub is_always_up: bool, pub is_always_up: bool,
pub windows: Vec<Window>, pub windows: Vec<Window>,
pub rarity: f32, pub rarity: f32,
pub filtered: bool,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -490,6 +491,7 @@ impl Data {
let mut cfish = CombinedFish { let mut cfish = CombinedFish {
entry: v, entry: v,
meta: m, meta: m,
filtered: false,
is_up: false, // fake default values for now is_up: false, // fake default values for now
is_always_up: false, // dito is_always_up: false, // dito
windows: Vec::new(), // dito windows: Vec::new(), // dito
@ -528,9 +530,10 @@ impl Filters {
&'a self, &'a self,
fish: Vec<&'a CombinedFish>, fish: Vec<&'a CombinedFish>,
caught_fish_ids: &[i32], caught_fish_ids: &[i32],
) -> Vec<&'a CombinedFish> { ) -> Vec<CombinedFish> {
fish.into_iter() fish.into_iter()
.filter(|fish| { .cloned()
.map(|fish| {
let f_caught = if self.include_caught { let f_caught = if self.include_caught {
true true
} else { } else {
@ -549,7 +552,10 @@ impl Filters {
true true
}; };
f_caught && f_patch && f_big CombinedFish {
filtered: !(f_caught && f_patch && f_big),
..fish
}
}) })
.collect() .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 { pub fn get_zone_name(data: &Data, id: u32) -> &str {
&data.db_data.zones.get(&id).unwrap().name_en &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)
}

View file

@ -85,7 +85,7 @@ async fn main_handler(
// Extract the list of fish we want to render. // Extract the list of fish we want to render.
let meta = state.data.fish_with_meta(); let meta = state.data.fish_with_meta();
let mut values: Vec<&CombinedFish> = filters.filter(meta.values().collect(), &caught_fish); let mut values: Vec<CombinedFish> = filters.filter(meta.values().collect(), &caught_fish);
values.sort_by(|afish, bfish| { values.sort_by(|afish, bfish| {
pinned_fish pinned_fish
.contains(&(bfish.entry.id as i32)) .contains(&(bfish.entry.id as i32))

View file

@ -11,7 +11,7 @@ use crate::{
pub struct ViewData<'a> { pub struct ViewData<'a> {
pub state: State<Arc<AppState>>, pub state: State<Arc<AppState>>,
pub fish: Vec<&'a CombinedFish<'a>>, pub fish: Vec<CombinedFish<'a>>,
pub caught_fish: Vec<i32>, pub caught_fish: Vec<i32>,
pub pinned_fish: Vec<i32>, pub pinned_fish: Vec<i32>,
pub acc_id: String, pub acc_id: String,
@ -75,129 +75,158 @@ pub fn main_page(data: ViewData) -> Markup {
pub fn fish_list(data: &ViewData) -> Markup { pub fn fish_list(data: &ViewData) -> Markup {
html! { html! {
@for fish in data.fish.clone() { @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))] { @if !fish.filtered {
.title { (fish_display(&fish, data))
div { @if !fish.entry.predators.is_empty() {
@if !data.caught_fish.contains(&(fish.entry.id as i32)) { .predators {
form action=(format!("/{}/catch/{}", data.acc_id, fish.entry.id)) method="post" { div.predators-header { small { "⇖ Requirements" } }
button.catch-button type="submit" { (PreEscaped("&check;")) } @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("&star;"))}
}
} @else {
form action=(format!("/{}/pin/{}/delete", data.acc_id, fish.entry.id)) method="post" {
button.pin-button type="submit" { (PreEscaped("&starf;"))}
}
}
}
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) { pub fn fish_display(fish: &CombinedFish, data: &ViewData) -> Markup {
.date.tiny data-ts=(clock::to_earth_time(window2.start_time).timestamp_millis()) { html! {
"next: " section.fish.up[fish.is_up].alwaysup[fish.is_always_up].pinned[data.pinned_fish.contains(&(fish.entry.id as i32))] {
.inner id=(format!("nextwindow-{}", fish.entry.id)) hx-preserve { .title {
(clock::to_earth_time(window2.start_time).format("%c %Z")) 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("&check;")) }
} @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 !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("&star;"))}
}
} @else {
form action=(format!("/{}/pin/{}/delete", data.acc_id, fish.entry.id)) method="post" {
button.pin-button type="submit" { (PreEscaped("&starf;"))}
}
}
} }
.how { div.name {
@for item_id in &fish.entry.best_catch_path { h3 {
@if let Some(item) = data.state.data.db_data.items.get(item_id) { (fish.meta.name_en)
span.catchpath title=(item.name_en) { span class=(format!("patch patch-{}", fish.entry.patch as u32)) { (fish.entry.patch) }
img src=(item.get_icon_url()) width="35";
@if let Some(hookset) = item.get_hookset(&data.state.data) { .subtitle {
img.hookset src=(hookset) width="20"; span { "Rarity: " (format!("{:.2}", fish.rarity * 100.)) "%" }
} }
}
@if let Some(tug) = item.get_tug(&data.state.data) { @if let Some(length) = fish.entry.intuition_length {
span.tug { (tug) } .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) } span.tug { (tug) }
} }
} }
} }
} }
.meta { @if let Some(hookset) = fish.entry.hookset {
@if let Some(location_id) = fish.entry.location { span.catchpath {
@if let Some(location) = data.state.data.db_data.fishing_spots.get(&location_id) { img src=(hookset) width="35";
div {
span.zone { (fish.meta.zone_en) } @if let Some(tug) = fish.entry.tug {
(location.name_en) 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 { div {
@if !fish.is_always_up { span.zone { (fish.meta.zone_en) }
"ET " (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.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 { } @else {
"always up!" "always up!"
}
} }
div { }
@if !fish.entry.weather_set.is_empty() { div {
@if !fish.entry.previous_weather_set.is_empty() { @if !fish.entry.weather_set.is_empty() {
@for weather in &fish.entry.previous_weather_set { @if !fish.entry.previous_weather_set.is_empty() {
img src=(get_weather_icon(&data.state.data, weather)) width="20" title=(get_weather_name(&data.state.data, weather)); @for weather in &fish.entry.previous_weather_set {
}
""
}
@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)); 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));
} }
} }
} }

BIN
static/intuition.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

View file

@ -46,6 +46,7 @@ select {
.title .subtitle { .title .subtitle {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: normal;
color: gray; color: gray;
} }
@ -219,3 +220,51 @@ section.pinned {
.pinned *:not(button) { .pinned *:not(button) {
color: white; 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;
}