GYVESXMMN2WA6ZE4NHRFIBVD5E62CWC4BGQHP24GYJQ6WJB2G2YAC
fn main() {
mount_to_body(move || view! { <App/> })
fn main() -> anyhow::Result<()> {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
let fmt_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_timer(OffsetTime::local_rfc_3339()?)
.with_writer(MakeWebConsoleWriter::new());
let perf_layer = performance_layer().with_details_from_fields(Pretty::default());
tracing_subscriber::registry()
.with(fmt_layer)
.with(perf_layer)
.init();
mount_to_body(move || view! { <App/> });
Ok(())
let player_1_balance = ColorBalance::default();
let player_1_deck = vec![];
let player_1_guardians = vec![];
let player_2_balance = ColorBalance {
suffering: 3,
divine: 3,
..Default::default()
};
let player_2_deck = vec![];
let player_2_guardians = vec![];
let player_decks: Vec<PlayerInitDescriptor> = vec![
(player_1_balance, player_1_deck, player_1_guardians).into(),
(player_2_balance, player_2_deck, player_2_guardians).into(),
];
let player_decks_len = player_decks.len();
pub mana_pool: HashMap<color::Color, usize>,
pub mana_pool: HashMap<Color, usize>,
pub balance: ColorBalance,
pub field: Field,
}
/// Wrapper that definitely refers to a hecs entity that *is* a card instance
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CardInstanced(hecs::Entity);
impl CardInstanced {}
/// Represents all the zones and places belonging to a player
#[derive(Debug, Default, Clone)]
pub struct Field {
magister_place: Option<CardInstanced>,
aide_places: [Option<CardInstanced>; 2],
fragment_places: [Option<CardInstanced>; 5],
// zones have no suffix
deck: VecDeque<CardInstanced>,
guardian_deck: VecDeque<CardInstanced>,
hand: VecDeque<CardInstanced>,
graveyard: VecDeque<CardInstanced>,
exile: VecDeque<CardInstanced>,
players: Vec<Player>,
stack: Stack,
turn: TurnState,
}
#[derive(thiserror::Error, Debug)]
pub enum GameCreationError {}
impl Game {
fn new<I>(players: I) -> Result<Self, GameCreationError>
where
I: IntoIterator<Item = PlayerInitDescriptor>,
{
let mut game = Self::default();
for (idx, player_desc) in players.into_iter().enumerate() {
_ = idx;
_ = player_desc;
// Validate that player_dec is valid
game.players.push(Player {
id: Ulid::new(),
mana_pool: HashMap::new(),
balance: player_desc.balance,
field: Field {
deck: vec![].into(),
guardian_deck: vec![].into(),
..Default::default()
},
})
}
Ok(game)
}
}
#[derive(Debug, Clone)]
struct PlayerInitDescriptor {
balance: ColorBalance,
deck: Vec<CardInstance>,
guardians: Vec<CardInstance>,
}
impl From<(ColorBalance, Vec<CardInstance>, Vec<CardInstance>)> for PlayerInitDescriptor {
fn from(
(balance, deck, guardians): (ColorBalance, Vec<CardInstance>, Vec<CardInstance>),
) -> Self {
Self {
deck,
guardians,
balance,
}
}
fn GameProvider(children: Children) -> impl IntoView {
let (game, set_game) = create_signal(Game::default());
fn GameProvider(
#[prop(into)] player_decks: MaybeSignal<Vec<PlayerInitDescriptor>>,
children: Children,
) -> impl IntoView {
let (game, set_game) = create_signal(Game::new(player_decks.get()));
//! Manages turns and phases
#[derive(Debug, Default)]
pub struct TurnState {
current_phase: Phase,
/// The game starts when this is 1.
current_turn: usize,
}
impl TurnState {
pub fn phase(&self) -> Phase {
self.current_phase
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Phase {
#[default]
Ready,
Standby,
Draw,
PrecombatMain,
Battle(BattleStep),
PostcombatMain,
End,
}
impl Phase {
/// Does this phase default to a closed GES?
pub fn is_closed(self) -> bool {
matches!(
self,
Phase::Ready | Phase::Battle(BattleStep::ResolveDamage)
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BattleStep {
BeginBattle,
DeclareAttackers,
MakeAttacks,
ResolveDamage,
EndBattle,
}
//! module for displaying current game state
use std::collections::HashMap;
use heck::ToTitleCase;
use leptos::{html::Canvas, *};
use plotters::{
chart::{ChartBuilder, LabelAreaPosition},
coord::ranged1d::{IntoSegmentedCoord, SegmentValue},
drawing::IntoDrawingArea,
series::Histogram,
style::{Color as _, RED, WHITE},
};
use plotters_canvas::CanvasBackend;
use web_sys::HtmlCanvasElement;
use crate::app::{color::Color, GameReader};
#[component]
pub fn Player(#[prop(into)] index: MaybeSignal<usize>) -> impl IntoView {
// Display the manapool and balance
const COLORS: &[Color] = &Color::all();
let game = expect_context::<GameReader>();
let player_data = move || {
game.0.with(|gs| {
let gs = gs.as_ref().ok()?;
Some(gs.players[index.get()].clone())
})
};
view! {
{move || {
player_data()
.map(|pd| {
let id = pd.id;
let balance = pd.balance;
let pool = pd.mana_pool;
let (gen_count, set_gen_count) = create_signal(1usize);
let (has_error, set_has_error) = create_signal(false);
let (gen_mana, set_gen_mana) = create_signal(None);
let (all_gen_mana, set_all_gen_mana) = create_signal(HashMap::new());
let plot_ref = create_node_ref::<Canvas>();
let plotted = move |plot_ref: HtmlElement<Canvas>, data: &[(Color, usize)]| {
let backend = CanvasBackend::with_canvas_object(
Clone::clone(HtmlCanvasElement::as_ref(&plot_ref)),
)
.expect("plotters canvas failed to initialize")
.into_drawing_area();
backend.fill(&WHITE.mix(1.0)).expect("failed to clear canvas");
let mut chart = ChartBuilder::on(&backend)
.set_label_area_size(LabelAreaPosition::Left, 40)
.set_label_area_size(LabelAreaPosition::Bottom, 40)
.build_cartesian_2d(
(0usize..6).into_segmented(),
0..all_gen_mana.get().values().copied().max().unwrap_or(1),
)
.expect("Failed to create chart");
const COLORS: &[Color] = &Color::all();
chart
.configure_mesh()
.disable_x_mesh()
.x_desc("Color")
.y_desc("Count")
.y_labels(5)
.x_labels(7)
.x_label_formatter(
&|idx| {
match idx {
SegmentValue::Exact(idx) => format!("{}?", COLORS[*idx]),
SegmentValue::CenterOf(idx) => {
COLORS
.get(*idx)
.map(ToString::to_string)
.unwrap_or(String::new())
}
SegmentValue::Last => String::new(),
}
},
)
.draw()
.expect("Failed to draw axises");
chart
.draw_series(
Histogram::vertical(&chart)
.style(RED.mix(0.5).filled())
.data(
data
.into_iter()
.map(|(c, i)| (
COLORS.iter().position(|oc| oc == c).unwrap(),
*i,
)),
),
)
.expect("Failed to draw data");
backend.present().expect("failed to present chart");
};
create_effect(move |_| {
let data = all_gen_mana.get();
if let Some(plot_ref) = plot_ref.get() {
plotted(plot_ref, &data.into_iter().collect::<Vec<_>>())
}
});
view! {
<div>
<h2 class="italic text-xl">
"Player " {move || index.get() + 1} " : "
<span class="font-mono not-italic bg-base-200 rounded p-1">
{move || format!("{id}")}
</span>
</h2>
<ul>
{COLORS
.iter()
.map(|c| {
view! {
<li>
{c.color().to_title_case()} " Mana: "
{pool.get(c).copied().unwrap_or(0)} " / (" {balance.get(*c)}
")"
</li>
}
})
.collect_view()}
</ul>
<div class="join">
<input
class="input input-bordered join-item"
type="number"
prop:value=move || gen_count.get()
class:has-error=has_error
on:input=move |ev| {
set_gen_count
.set(
match event_target_value(&ev).parse() {
Ok(val) => {
set_has_error.set(false);
val
}
Err(_e) => {
set_has_error.set(true);
return;
}
},
);
}
/>
<button
class="btn join-item"
on:click=move |_| {
let generated_mana: Vec<_> = balance
.gen_mana(&mut rand::thread_rng())
.take(gen_count.get())
.collect();
let counts = Color::all()
.into_iter()
.map(|c| (
c,
generated_mana.iter().filter(|col| col == &&c).count(),
))
.collect::<HashMap<_, _>>();
set_all_gen_mana
.update(|all_gen_mana| {
all_gen_mana.extend(counts);
});
set_gen_mana.set(Some(generated_mana));
}
>
"Generate Mana"
</button>
</div>
<div class="font-mono">
{move || {
if let Some(gen_mana) = gen_mana.get() {
let mapped = Color::all()
.into_iter()
.map(|c| (
c,
gen_mana.iter().filter(|col| col == &&c).count(),
))
.collect::<HashMap<_, _>>();
format!("{mapped:?}")
} else {
"".to_string()
}
}}
</div>
<Show when=move || {
all_gen_mana.with(|agm| agm.values().copied().sum::<usize>() > 0)
}>
<canvas width="600" height="200" _ref=plot_ref></canvas>
</Show>
</div>
}
})
}}
}
}
pub fn get(&self, color: Color) -> usize {
match color {
Color::Divine => self.divine,
Color::Revelation => self.revelation,
Color::Grace => self.grace,
Color::Growth => self.growth,
Color::Crusade => self.crusade,
Color::Suffering => self.suffering,
Color::Mundane => self.mundane,
}
}
self.mundane.max(1),
self.divine.max(1),
self.revelation.max(1),
self.grace.max(1),
self.growth.max(1),
self.crusade.max(1),
self.suffering.max(1),
self.mundane.saturating_add(1),
self.divine.saturating_add(1),
self.revelation.saturating_add(1),
self.grace.saturating_add(1),
self.growth.saturating_add(1),
self.crusade.saturating_add(1),
self.suffering.saturating_add(1),
#[derive(Debug, Clone)]
pub enum Card {}
#[derive(Debug, Clone, Copy)]
pub struct CardInstance {}
//! Handling of abilities and effects
use std::collections::VecDeque;
/// Contains stack items
#[derive(Debug, Default)]
pub struct Stack {
/// Are we in the middle of resolving an effect?
is_resolving: bool,
stack: VecDeque<StackItem>,
}
#[derive(Debug, Clone)]
pub enum StackItem {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GameEffectState {
Closed,
HalfOpen,
Open,
}
impl Stack {
// The only way to override this is to look are the Phase::is_closed result
// If that is true, GES is closed no matter what
pub fn game_effect_state(&self) -> GameEffectState {
if self.stack.is_empty() {
GameEffectState::Open
} else if self.is_resolving {
GameEffectState::Closed
} else {
// The stack is *not* empty, but we are *not* in the middle of effect resolution
GameEffectState::HalfOpen
}
}
}