Fix heatmap issue and implement per round analysis

This commit is contained in:
Lol3rrr
2024-10-08 23:15:42 +02:00
parent 4353de4455
commit 539adecf5d
24 changed files with 58455 additions and 335 deletions

View File

@@ -2,6 +2,8 @@ use leptos::*;
use leptos_router::{Outlet, A};
pub mod heatmap;
pub mod scoreboard;
pub mod perround;
#[derive(Debug, Clone)]
struct CurrentDemoName(ReadSignal<String>);
@@ -87,95 +89,3 @@ pub fn demo() -> impl leptos::IntoView {
</div>
}
}
#[leptos::component]
pub fn scoreboard() -> impl leptos::IntoView {
use leptos::Suspense;
let scoreboard_resource =
create_resource(leptos_router::use_params_map(), |params| async move {
let id = params.get("id").unwrap();
let res =
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id))
.send()
.await
.unwrap();
res.json::<common::demo_analysis::ScoreBoard>()
.await
.unwrap()
});
let team_display_func = |team: &[common::demo_analysis::ScoreBoardPlayer]| {
team.iter()
.map(|player| {
view! {
<tr>
<td> { player.name.clone() } </td>
<td> { player.kills } </td>
<td> { player.assists } </td>
<td> { player.deaths } </td>
<td> { player.damage } </td>
</tr>
}
})
.collect::<Vec<_>>()
};
view! {
<h2>Scoreboard</h2>
<Suspense
fallback=move || view! { <p>Loading Scoreboard data</p> }
>
<div>
<h3>Team 1</h3>
<table>
<tr>
<th>Name</th>
<th>Kills</th>
<th>Assists</th>
<th>Deaths</th>
<th>Damage</th>
</tr>
{
move || {
scoreboard_resource.get().map(|s| {
let team = s.team1;
team_display_func(&team)
})
}
}
</table>
</div>
<div>
<h3>Team 2</h3>
<table>
<tr>
<th>Name</th>
<th>Kills</th>
<th>Assists</th>
<th>Deaths</th>
<th>Damage</th>
</tr>
{
move || {
scoreboard_resource.get().map(|s| {
let team = s.team2;
team_display_func(&team)
})
}
}
</table>
</div>
</Suspense>
}
}
#[leptos::component]
pub fn per_round() -> impl leptos::IntoView {
view! {
<h3>Per Round</h3>
}
}

View File

@@ -18,20 +18,28 @@ pub fn heatmaps() -> impl leptos::IntoView {
.unwrap()
});
view! {
<h3>Heatmaps</h3>
<Suspense fallback=move || view! { <p>Loading Heatmaps</p> }>
<div>
{
move || {
heatmaps_resource.get().map(|h| {
view! { <HeatmapView heatmaps=h /> }
})
}
let style = stylers::style! {
"Heatmap-Wrapper",
.container {
margin-top: 1vh;
}
</div>
</Suspense>
};
view! {
class=style,
<div class="container">
<Suspense fallback=move || view! { <p>Loading Heatmaps</p> }>
<div>
{
move || {
heatmaps_resource.get().map(|h| {
view! { <HeatmapView heatmaps=h /> }
})
}
}
</div>
</Suspense>
</div>
}
}
@@ -59,12 +67,21 @@ fn heatmap_view(heatmaps: Vec<common::demo_analysis::PlayerHeatmap>) -> impl lep
.heatmap_image > .heatmap {
opacity: 0.5;
}
.heatmap_image > img {
width: min(70vw, 70vh);
height: min(70vh, 70vw);
}
.player_select {
width: min(70vw, 70vh);
}
};
view! {
class=style,
<div>
<select on:change=move |ev| {
<select class="player_select" on:change=move |ev| {
let new_value = event_target_value(&ev);
let idx: usize = new_value.parse().unwrap();
set_value(heatmaps.get(idx).cloned());
@@ -87,7 +104,7 @@ fn heatmap_view(heatmaps: Vec<common::demo_analysis::PlayerHeatmap>) -> impl lep
class=style,
<div class="heatmap_image">
<img class="radar" src=format!("/static/minimaps/{}.png", map.0.get()) />
<img class="heatmap" width=1024 height=1024 src=format!("data:image/png;base64,{}", heatmap.png_data) />
<img class="heatmap" src=format!("data:image/png;base64,{}", heatmap.png_data) />
</div>
}.into_any(),
None => view! { <p>ERROR</p> }.into_any(),

View File

@@ -0,0 +1,143 @@
use leptos::*;
fn to_roman(mut number: u32) -> char {
if number < 12 {
char::from_u32(8544 + number).unwrap()
} else if number < 24 {
char::from_u32(8544 + (number - 12)).unwrap()
} else if number < 27 {
char::from_u32(8544 + (number - 24)).unwrap()
} else {
char::from_u32(8544 + (number - 27)).unwrap()
}
}
fn to_coloumn(idx: usize) -> usize {
if idx < 12 {
1 + idx
} else if idx < 24 {
1 + idx + 1
} else if idx < 27 {
1 + idx + 2
} else {
1 + idx + 3
}
}
#[leptos::component]
pub fn per_round() -> impl leptos::IntoView {
let perround_resource =
create_resource(leptos_router::use_params_map(), |params| async move {
let id = params.get("id").unwrap();
let res =
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/perround", id))
.send()
.await
.unwrap();
res.json::<Vec<common::demo_analysis::DemoRound>>()
.await
.unwrap()
});
let style = stylers::style! {
"PerRound",
.round_overview {
display: inline-grid;
width: 90vw;
grid-template-columns: repeat(12, 1fr) 5px repeat(12, 1fr) 5px repeat(3, 1fr) 5px repeat(3, 1fr);
grid-template-rows: repeat(3, auto);
}
.round_entry {
display: inline-block;
border-left: 1px solid #101010;
border-right: 1px solid #101010;
padding-left: 2px;
padding-right: 2px;
}
.round_number {
border-top: 1px solid #101010;
border-bottom: 1px solid #101010;
}
p.round_entry {
margin: 0px;
}
};
let (round, set_round) = create_signal(0);
let events_list = move || {
let round_index = round();
let current_round = perround_resource.get().map(|rs| rs.get(round_index).cloned()).flatten();
match current_round {
Some(round) => {
round.events.iter().map(|event| {
match event {
common::demo_analysis::RoundEvent::BombPlanted => view! { <li>Bomb has been planted</li> }.into_view(),
common::demo_analysis::RoundEvent::BombDefused => view! { <li>Bomb has been defused</li> }.into_view(),
common::demo_analysis::RoundEvent::Killed { attacker, died } => view! { <li>{"'"}{ attacker }{"'"} killed {"'"}{ died }{"'"}</li> }.into_view(),
}
}).collect::<Vec<_>>().into_view()
}
None => view! {}.into_view(),
}
};
let round_overview = move || {
(0..30).map(|r| {
let set_round = move |_| {
set_round.set(r);
};
let round = perround_resource.get().map(|rs| rs.get(r).cloned()).flatten();
let reason = round.map(|r| r.reason);
// Upper is CT by default and lower is T by default
let mut upper_symbol = match &reason {
Some(common::demo_analysis::RoundWinReason::TKilled) => view! { <span>Killed Ts</span> }.into_view(),
Some(common::demo_analysis::RoundWinReason::BombDefused) => view! { <span>Defused</span> }.into_view(),
Some(common::demo_analysis::RoundWinReason::TimeRanOut) => view! { <span>Out of Time</span> }.into_view(),
_ => view! {}.into_view(),
};
let mut lower_symbol = match &reason {
Some(common::demo_analysis::RoundWinReason::CTKilled) => view! { <span>Killed CTs</span> }.into_view(),
Some(common::demo_analysis::RoundWinReason::BombExploded) => view! { <span>Exploded</span> }.into_view(),
_ => view! {}.into_view(),
};
if (12..27).contains(&r) {
core::mem::swap(&mut upper_symbol, &mut lower_symbol);
}
view! {
class=style,
<div class="round_entry" style=format!("grid-column: {}; grid-row: 1", to_coloumn(r))> { upper_symbol } </div>
<p on:click=set_round class="round_entry round_number" style=format!("grid-column: {}; grid-row: 2", to_coloumn(r))>{ r + 1 }</p>
<div class="round_entry" style=format!("grid-column: {}; grid-row: 3", to_coloumn(r))> { lower_symbol } </div>
}
}).collect::<Vec<_>>()
};
view! {
class=style,
<h3>Per Round</h3>
<div class="round_overview">
{ round_overview }
</div>
<div>
<h3> Round { move || round.get() + 1 } </h3>
<div>
<ul> { events_list } </ul>
</div>
</div>
}
}

View File

@@ -0,0 +1,76 @@
use leptos::*;
#[leptos::component]
pub fn scoreboard() -> impl leptos::IntoView {
use leptos::Suspense;
let scoreboard_resource =
create_resource(leptos_router::use_params_map(), |params| async move {
let id = params.get("id").unwrap();
let res =
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id))
.send()
.await
.unwrap();
res.json::<common::demo_analysis::ScoreBoard>()
.await
.unwrap()
});
view! {
<h2>Scoreboard</h2>
<Suspense
fallback=move || view! { <p>Loading Scoreboard data</p> }
>
<TeamScoreboard info=scoreboard_resource team_name="Team 1".to_string() part=|s| s.team1 />
<TeamScoreboard info=scoreboard_resource team_name="Team 2".to_string() part=|s| s.team2 />
</Suspense>
}
}
#[leptos::component]
fn team_scoreboard(info: Resource<leptos_router::ParamsMap, common::demo_analysis::ScoreBoard>, team_name: String, part: fn(common::demo_analysis::ScoreBoard) -> Vec<common::demo_analysis::ScoreBoardPlayer>) -> impl IntoView {
let style = stylers::style! {
"Team-Scoreboard",
tr:nth-child(even) {
background-color: #dddddd;
}
th {
padding-left: 10px;
padding-right: 10px;
}
th:nth-child(1) {
width: 200px;
}
};
view! {
class = style,
<div>
<h3>{ team_name }</h3>
<table>
<tr>
<th>Name</th>
<th>Kills</th>
<th>Assists</th>
<th>Deaths</th>
<th>Damage</th>
</tr>
{
move || {
let value = info.get().map(|v| part(v));
(value).into_iter().flat_map(|v| v).map(|s| {
view! {
class=style,
<tr><td>{ s.name }</td><td>{ s.kills }</td><td>{ s.assists }</td><td>{ s.deaths }</td><td>{ s.damage }</td></tr>
}
}).collect::<Vec<_>>()
}
}
</table>
</div>
}
}

View File

@@ -3,16 +3,6 @@ use leptos_router::*;
use frontend::{Demo, Homepage, TopBar, UploadDemo};
async fn load_demos() -> Vec<common::BaseDemoInfo> {
let res = reqwasm::http::Request::get("/api/demos/list")
.send()
.await
.unwrap();
let demos: Vec<common::BaseDemoInfo> = res.json().await.unwrap();
demos
}
fn main() {
let (upload_demo_read, upload_demo_write) = create_signal(frontend::DemoUploadStatus::Hidden);
@@ -28,10 +18,10 @@ fn main() {
<Routes>
<Route path="/" view=Homepage />
<Route path="/demo/:id" view=Demo>
<Route path="perround" view=frontend::demo::PerRound />
<Route path="scoreboard" view=frontend::demo::Scoreboard />
<Route path="perround" view=frontend::demo::perround::PerRound />
<Route path="scoreboard" view=frontend::demo::scoreboard::Scoreboard />
<Route path="heatmaps" view=frontend::demo::heatmap::Heatmaps />
<Route path="" view=frontend::demo::Scoreboard />
<Route path="" view=frontend::demo::scoreboard::Scoreboard />
</Route>
</Routes>
</main>