Macos builds working

Includes fmod (dynamic linking) and pretty dmg installer
main
Elijah Voigt 1 year ago committed by Elijah Voigt
parent 061ba38092
commit 90584babb6

@ -14,14 +14,12 @@ rustflags = [
# `brew install llvm` # `brew install llvm`
[target.x86_64-apple-darwin] [target.x86_64-apple-darwin]
rustflags = [ rustflags = [
"-Clink-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld", # Use LLD Linker
"-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
"-Zthreads=0", # (Nightly) Use improved multithreading with the recommended amount of threads. "-Zthreads=0", # (Nightly) Use improved multithreading with the recommended amount of threads.
] ]
[target.aarch64-apple-darwin] [target.aarch64-apple-darwin]
rustflags = [ rustflags = [
"-Clink-arg=-fuse-ld=/opt/homebrew/opt/llvm/bin/ld64.lld", # Use LLD Linker
"-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations "-Zshare-generics=y", # (Nightly) Make the current crate share its generic instantiations
"-Zthreads=0", # (Nightly) Use improved multithreading with the recommended amount of threads. "-Zthreads=0", # (Nightly) Use improved multithreading with the recommended amount of threads.
] ]

4
.gitattributes vendored

@ -1,5 +1,3 @@
fmodstudio/linux filter=lfs diff=lfs merge=lfs -text
fmodstudio/windows filter=lfs diff=lfs merge=lfs -text
assets/ filter=lfs diff=lfs merge=lfs -text assets/ filter=lfs diff=lfs merge=lfs -text
*.so filter=lfs diff=lfs merge=lfs -text *.so filter=lfs diff=lfs merge=lfs -text
*.so.* filter=lfs diff=lfs merge=lfs -text *.so.* filter=lfs diff=lfs merge=lfs -text
@ -18,3 +16,5 @@ assets/ filter=lfs diff=lfs merge=lfs -text
*.lib filter=lfs diff=lfs merge=lfs -text *.lib filter=lfs diff=lfs merge=lfs -text
*.gltf filter=lfs diff=lfs merge=lfs -text *.gltf filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text
*.dylib filter=lfs diff=lfs merge=lfs -text
*.dylib filter=lfs diff=lfs merge=lfs -text

9
.gitignore vendored

@ -30,3 +30,12 @@ temp/
*.zip *.zip
*.tar.gz *.tar.gz
.DS_Store
*.dmg
!platforms/macos/Martian\ Chess.app/Contents/Info.plist
platforms/macos
icon_*.png
*.iconset
*.icns

3
Cargo.lock generated

@ -655,8 +655,7 @@ dependencies = [
[[package]] [[package]]
name = "bevy_fmod" name = "bevy_fmod"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/Salzian/bevy_fmod.git?branch=main#bba24000626d251b1e1582d8b3863caa6dc701a6"
checksum = "52cb5dfc9e27a6b8be8cabb6925ba1731e2a7f02bdc0bc774ba3b6b652613f46"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bevy", "bevy",

@ -1,11 +1,12 @@
[package] [package]
name = "martian-chess" name = "martian-chess"
description = "Martian Chess videogame"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
[dependencies] [dependencies]
bevy_fmod = { version = "0.4" } bevy_fmod = { git = "https://github.com/Salzian/bevy_fmod.git", branch = "main" }
bevy = { version = "0.13", features = ["jpeg", "hdr", "serialize"] } bevy = { version = "0.13", features = ["jpeg", "hdr", "serialize"] }
serde = "1" serde = "1"
toml = { version = "0.8", features = ["parse"] } toml = { version = "0.8", features = ["parse"] }
@ -30,4 +31,4 @@ opt-level = "z"
strip = true strip = true
panic = "abort" panic = "abort"
lto = true lto = true
codegen-units = 1 codegen-units = 1

@ -2,6 +2,9 @@ fn main() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
println!("cargo:rustc-link-search=lib/linux"); println!("cargo:rustc-link-search=lib/linux");
#[cfg(target_os = "macos")]
println!("cargo:rustc-link-search=lib/macos");
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
println!("cargo:rustc-link-search=lib/windows"); println!("cargo:rustc-link-search=lib/windows");

BIN
lib/macos/libfmod.dylib (Stored with Git LFS)

Binary file not shown.

BIN
lib/macos/libfmodstudio.dylib (Stored with Git LFS)

Binary file not shown.

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Martian Chess</string>
<key>CFBundleExecutable</key>
<string>MartianChess</string>
<key>CFBundleIconFile</key>
<string>MartianChess.icns</string>
<key>CFBundleIdentifier</key>
<string>your.domain.bevy-game</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MartianChess</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>LSMinimumSystemVersion</key>
<string>10.9</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
</dict>
</plist>

@ -0,0 +1,5 @@
#!/bin/bash
# Generate x86 and arm builds
cargo build --release --target aarch64-apple-darwin
cargo build --release --target x86_64-apple-darwin

@ -0,0 +1,34 @@
#!/bin/bash
# https://gist.github.com/ansarizafar/6fa64f44aa933794c4d6638eec32b9aa
BASE="media/MartianChess.png"
OUT_DIR="media/MartianChess.iconset"
mkdir -p $OUT_DIR
# 1024x1024
sips "$BASE" -Z 1024 -o $OUT_DIR/icon_512x512@2.png
# 512x512
sips "$BASE" -Z 512 -o $OUT_DIR/icon_512x512.png
sips "$BASE" -Z 512 -o $OUT_DIR/icon_256x256@2.png
# 256x256
sips "$BASE" -Z 256 -o $OUT_DIR/icon_256x256.png
sips "$BASE" -Z 256 -o $OUT_DIR/icon_128x128@2.png
# 128x128
sips "$BASE" -Z 128 -o $OUT_DIR/icon_128x128.png
sips "$BASE" -Z 128 -o $OUT_DIR/icon_64x64@2.png
# 64x64
sips "$BASE" -Z 64 -o $OUT_DIR/icon_64x64.png
sips "$BASE" -Z 64 -o $OUT_DIR/icon_32x32@2.png
# 32x32
sips "$BASE" -Z 32 -o $OUT_DIR/icon_32x32.png
sips "$BASE" -Z 32 -o $OUT_DIR/icon_16x16@2.png
# 16x16
sips "$BASE" -Z 16 -o $OUT_DIR/icon_16x16.png
iconutil -c icns $OUT_DIR

@ -0,0 +1,51 @@
#!/bin/bash
# https://github.com/create-dmg/create-dmg/tree/master?tab=readme-ov-file#create-dmg
NAME='Martian Chess.app'
APP="platforms/macos/$NAME"
CONTENTS="$APP/Contents"
GAME="$CONTENTS/MacOS"
RESOURCES="$CONTENTS/Resources"
FRAMEWORKS="$CONTENTS/Frameworks"
mkdir -p "$APP"
mkdir -p "$GAME"
mkdir -p "$RESOURCES"
mkdir -p "$FRAMEWORKS"
# Copy icons to package
cp -f media/MartianChess.icns "$RESOURCES"
# Generate cross-architecture binary
rm -f "$GAME/MartianChess"
lipo "target/x86_64-apple-darwin/release/martian-chess" \
"target/aarch64-apple-darwin/release/martian-chess" \
-create -output "$GAME/MartianChess"
# Copy assets
rm -rf "$GAME/assets"
cp -r assets "$GAME/"
# Copy fmod libraries
cp -f lib/macos/libfmod.dylib "$FRAMEWORKS/"
cp -f lib/macos/libfmodstudio.dylib "$FRAMEWORKS/"
# Update dynamic linking search paths
install_name_tool -change @rpath/libfmod.dylib "@loader_path/../Frameworks/libfmod.dylib" "$GAME/MartianChess"
install_name_tool -change @rpath/libfmodstudio.dylib "@loader_path/../Frameworks/libfmodstudio.dylib" "$GAME/MartianChess"
# Build dmg file
rm -f packages/martian_chess.dmg
create-dmg \
--volname "Martian Chess" \
--volicon "media/MartianChess.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--app-drop-link 600 200 \
--icon-size 100 \
--icon "$NAME" 200 190 \
--hide-extension "$NAME" \
--app-drop-link 600 185 \
"packages/Martian Chess.dmg" \
./platforms/macos/

@ -5,42 +5,41 @@ pub(crate) struct AiPlugin;
impl Plugin for AiPlugin { impl Plugin for AiPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_state::<PlayState>() app.init_state::<PlayState>()
.init_resource::<AiMove>() .init_resource::<AiMove>()
.init_state::<AiDrama>() .init_state::<AiDrama>()
// Bogo AI Systems // Bogo AI Systems
.add_systems( .add_systems(
Update, Update,
( (
bogo_ai_thinking bogo_ai_thinking.run_if(in_state(AiDrama::Thinking)),
.run_if(in_state(AiDrama::Thinking)), bogo_ai_holding.run_if(in_state(AiDrama::Holding)),
bogo_ai_holding )
.run_if(in_state(AiDrama::Holding)), .run_if(in_state(PlayState::AiBogo))
) .run_if(in_state(GameState::Play))
.run_if(in_state(PlayState::AiBogo)) .run_if(in_state(TurnState(Side::A)))
.run_if(in_state(GameState::Play)) .run_if(
.run_if(in_state(TurnState(Side::A))) in_state(TutorialState::None)
.run_if(in_state(TutorialState::None) .or_else(in_state(TutorialState::Empty))
.or_else(in_state(TutorialState::Empty)) .or_else(in_state(TutorialState::PieceEnd)),
.or_else(in_state(TutorialState::PieceEnd)) ),
) );
); }
}
} }
#[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)] #[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)]
pub(crate) enum PlayState { pub(crate) enum PlayState {
Human, Human,
#[default] #[default]
AiBogo, AiBogo,
} }
#[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone)] #[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone)]
enum AiDrama { enum AiDrama {
// Before AI picks up a piece // Before AI picks up a piece
#[default] #[default]
Thinking, Thinking,
// When AI picks up piece // When AI picks up piece
Holding, Holding,
} }
/// The move the AI will commit /// The move the AI will commit
@ -49,124 +48,126 @@ struct AiMove(Move);
// Bogo AI logic // Bogo AI logic
fn bogo_ai_thinking( fn bogo_ai_thinking(
board: Res<Board>, board: Res<Board>,
query: Query<(Entity, &BoardIndex), With<Piece>>, query: Query<(Entity, &BoardIndex), With<Piece>>,
time: Res<Time>, time: Res<Time>,
mut next_drama: ResMut<NextState<AiDrama>>, mut next_drama: ResMut<NextState<AiDrama>>,
mut timer: Local<Timer>, mut timer: Local<Timer>,
mut commands: Commands, mut commands: Commands,
mut selected: ResMut<AiMove>, mut selected: ResMut<AiMove>,
tweaks: Res<Assets<Tweaks>>, tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>, tweaks_file: Res<tweak::GameTweaks>,
) { ) {
let tweak = tweaks let tweak = tweaks
.get(tweaks_file.handle.clone()) .get(tweaks_file.handle.clone())
.expect("Load tweakfile"); .expect("Load tweakfile");
let lag = tweak.get::<f32>("ai_lag_secs").unwrap(); let lag = tweak.get::<f32>("ai_lag_secs").unwrap();
let timer_duration = Duration::from_secs_f32(lag); let timer_duration = Duration::from_secs_f32(lag);
// Initialize timer // Initialize timer
if timer.duration() != timer_duration { if timer.duration() != timer_duration {
timer.set_duration(Duration::from_secs_f32(1.0)); timer.set_duration(Duration::from_secs_f32(1.0));
timer.set_mode(TimerMode::Once); timer.set_mode(TimerMode::Once);
} }
// Timer before committing move // Timer before committing move
if !timer.finished() { if !timer.finished() {
timer.tick(time.delta()); timer.tick(time.delta());
} else { } else {
debug!("AI finished thinking"); debug!("AI finished thinking");
debug!("Timer: {:?}", timer); debug!("Timer: {:?}", timer);
// TODO: Assuming AI is Side::A // TODO: Assuming AI is Side::A
let ai_side = Side::A; let ai_side = Side::A;
// Determine which moves to do // Determine which moves to do
let (from, to) = { let (from, to) = {
let on = board.on(ai_side); let on = board.on(ai_side);
let set: Vec<(&BoardIndex, HashSet<BoardIndex>)> = on let set: Vec<(&BoardIndex, HashSet<BoardIndex>)> = on
.iter() .iter()
.filter_map(|(_, idx)| { .filter_map(|(_, idx)| {
let moves = board.valid_moves(*idx); let moves = board.valid_moves(*idx);
(!moves.is_empty()).then_some((idx, moves)) (!moves.is_empty()).then_some((idx, moves))
}) })
.collect(); .collect();
let r = time.elapsed_seconds() as usize; let r = time.elapsed_seconds() as usize;
let m_i = r % set.len(); let m_i = r % set.len();
let m = set.get(m_i).unwrap(); let m = set.get(m_i).unwrap();
let m_bi = m.0; let m_bi = m.0;
let n_i = r % m.1.len(); let n_i = r % m.1.len();
let n_bi = m.1.iter().nth(n_i).unwrap(); let n_bi = m.1.iter().nth(n_i).unwrap();
(m_bi.clone(), n_bi.clone()) (m_bi.clone(), n_bi.clone())
}; };
// Pick up selected piece // Pick up selected piece
query query
.iter() .iter()
.find_map(|(e, bi)| { .find_map(|(e, bi)| (*bi == from).then_some(e))
(*bi == from).then_some(e) .iter()
}) .for_each(|e| {
.iter() commands.entity(*e).insert(Selected);
.for_each(|e| { });
commands.entity(*e).insert(Selected);
}); *selected = AiMove(Move {
from,
*selected = AiMove(Move { from, to: Some(to), ..default() }); to: Some(to),
..default()
// Pass off to next state in the "drama" });
next_drama.set(AiDrama::Holding);
// Pass off to next state in the "drama"
// Reset timer for next time next_drama.set(AiDrama::Holding);
timer.reset();
} // Reset timer for next time
timer.reset();
}
} }
fn bogo_ai_holding( fn bogo_ai_holding(
selected: Res<AiMove>, selected: Res<AiMove>,
time: Res<Time>, time: Res<Time>,
mut timer: Local<Timer>, mut timer: Local<Timer>,
mut board: ResMut<Board>, mut board: ResMut<Board>,
mut move_events: EventWriter<Move>, mut move_events: EventWriter<Move>,
mut next_drama: ResMut<NextState<AiDrama>>, mut next_drama: ResMut<NextState<AiDrama>>,
tweaks: Res<Assets<Tweaks>>, tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>, tweaks_file: Res<tweak::GameTweaks>,
) { ) {
let tweak = tweaks let tweak = tweaks
.get(tweaks_file.handle.clone()) .get(tweaks_file.handle.clone())
.expect("Load tweakfile"); .expect("Load tweakfile");
let lag = tweak.get::<f32>("ai_lag_secs").unwrap(); let lag = tweak.get::<f32>("ai_lag_secs").unwrap();
let timer_duration = Duration::from_secs_f32(lag); let timer_duration = Duration::from_secs_f32(lag);
// Initialize timer // Initialize timer
if timer.duration() != timer_duration { if timer.duration() != timer_duration {
timer.set_duration(Duration::from_secs_f32(1.0)); timer.set_duration(Duration::from_secs_f32(1.0));
timer.set_mode(TimerMode::Once); timer.set_mode(TimerMode::Once);
} }
// Timer before committing move // Timer before committing move
if !timer.finished() { if !timer.finished() {
timer.tick(time.delta()); timer.tick(time.delta());
} else { } else {
debug!("AI Holding"); debug!("AI Holding");
debug!("Timer: {:?}", timer); debug!("Timer: {:?}", timer);
// Apply selected moves // Apply selected moves
board board
.move_piece(selected.0.from, selected.0.to.unwrap()) .move_piece(selected.0.from, selected.0.to.unwrap())
.unwrap() .unwrap()
.iter() .iter()
.for_each(|ai_move| { .for_each(|ai_move| {
// De-select the piece // De-select the piece
move_events.send(ai_move.clone()); move_events.send(ai_move.clone());
}); });
// Pass off to next state in the "drama" // Pass off to next state in the "drama"
next_drama.set(AiDrama::Thinking); next_drama.set(AiDrama::Thinking);
timer.reset(); timer.reset();
} }
} }

@ -9,14 +9,12 @@ impl Plugin for AudioPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<AudioEvent>(); app.add_event::<AudioEvent>();
app.add_plugins(FmodPlugin { app.add_plugins(FmodPlugin::new(&[
audio_banks_paths: &[ "./assets/audio/Martian Chess Audio/Build/Desktop/Master.bank",
"./assets/audio/Martian Chess Audio/Build/Desktop/Master.bank", "./assets/audio/Martian Chess Audio/Build/Desktop/Master.strings.bank",
"./assets/audio/Martian Chess Audio/Build/Desktop/Master.strings.bank", "./assets/audio/Martian Chess Audio/Build/Desktop/Music.bank",
"./assets/audio/Martian Chess Audio/Build/Desktop/Music.bank", "./assets/audio/Martian Chess Audio/Build/Desktop/SFX.bank",
"./assets/audio/Martian Chess Audio/Build/Desktop/SFX.bank", ]));
],
});
app.init_resource::<AudioVolume>(); app.init_resource::<AudioVolume>();
app.add_systems(OnEnter(GameState::Intro), play_background); app.add_systems(OnEnter(GameState::Intro), play_background);

@ -121,20 +121,23 @@ fn update_credits(
text.sections = { text.sections = {
credits_text credits_text
.split('\n') .split('\n')
.map(|line| { .map(|line| match line.strip_prefix("# ") {
match line.strip_prefix("# ") { Some(title) => TextSection {
Some(title) => TextSection { value: format!("{}\n", title),
value: format!("{}\n", title), style: TextStyle {
style: TextStyle { font_size: title_font_size, ..default() }, font_size: title_font_size,
}, ..default()
None => { },
TextSection { },
value: format!("{}\n", line), None => TextSection {
style: TextStyle { font_size, ..default() }, value: format!("{}\n", line),
} style: TextStyle {
} font_size,
} ..default()
}).collect() },
},
})
.collect()
}; };
debug!("Text sections: {:?}", text.sections); debug!("Text sections: {:?}", text.sections);
}); });

@ -31,7 +31,7 @@ impl Plugin for DebugPlugin {
toggle_debug_mode.run_if(on_event::<KeyboardInput>()), toggle_debug_mode.run_if(on_event::<KeyboardInput>()),
display_diagnostics.run_if(in_state(DebugState::Enabled)), display_diagnostics.run_if(in_state(DebugState::Enabled)),
toggle_debug_ui.run_if(state_changed::<DebugState>), toggle_debug_ui.run_if(state_changed::<DebugState>),
aspect_ratio.run_if(on_event::<WindowResized>()) aspect_ratio.run_if(on_event::<WindowResized>()),
), ),
); );
} }
@ -261,14 +261,11 @@ fn debug_piece(
} }
} }
fn aspect_ratio( fn aspect_ratio(mut debug_info: ResMut<DebugInfo>, window: Query<&Window>) {
mut debug_info: ResMut<DebugInfo>,
window: Query<&Window>,
) {
window.iter().for_each(|window| { window.iter().for_each(|window| {
let x = window.resolution.width(); let x = window.resolution.width();
let y = window.resolution.height(); let y = window.resolution.height();
let aspect_ratio = format!("{}x{}", x, y); let aspect_ratio = format!("{}x{}", x, y);
debug_info.set("aspect Ratio".into(), aspect_ratio); debug_info.set("aspect Ratio".into(), aspect_ratio);
}) })
} }

@ -19,18 +19,11 @@ impl Plugin for Display3dPlugin {
color: Color::WHITE, color: Color::WHITE,
brightness: 100.0, brightness: 100.0,
}) })
.insert_resource(PointLightShadowMap { .insert_resource(PointLightShadowMap { size: 512 })
size: 512 .insert_resource(DirectionalLightShadowMap { size: 512 })
})
.insert_resource(DirectionalLightShadowMap {
size: 512
})
.add_systems( .add_systems(
OnExit(GameState::Loading), OnExit(GameState::Loading),
( (fix_skybox, initialize.after(fix_skybox)),
fix_skybox,
initialize.after(fix_skybox),
),
) )
// Systems related to color and camera // Systems related to color and camera
.add_systems( .add_systems(
@ -70,7 +63,7 @@ impl Plugin for Display3dPlugin {
pick_up.run_if(any_component_added::<game::Selected>()), pick_up.run_if(any_component_added::<game::Selected>()),
de_select.run_if(just_pressed(MouseButton::Right)), de_select.run_if(just_pressed(MouseButton::Right)),
put_down.run_if(any_component_removed::<game::Selected>()), put_down.run_if(any_component_removed::<game::Selected>()),
) ),
) )
.add_systems( .add_systems(
Update, Update,
@ -86,8 +79,9 @@ impl Plugin for Display3dPlugin {
switch_sides switch_sides
.run_if(in_state(GameState::Play)) .run_if(in_state(GameState::Play))
.run_if(state_changed::<game::TurnState>) .run_if(state_changed::<game::TurnState>)
.run_if(any_component_added::<Selected>() .run_if(
.or_else(any_component_removed::<Selected>()) any_component_added::<Selected>()
.or_else(any_component_removed::<Selected>()),
) )
.run_if(should_switch_sides), .run_if(should_switch_sides),
// Camera moving up when first piece is selected in the game // Camera moving up when first piece is selected in the game
@ -106,14 +100,14 @@ impl Plugin for Display3dPlugin {
.or_else(any_component_changed::<BoardIndex>()) .or_else(any_component_changed::<BoardIndex>())
.or_else(any_component_removed::<Animating>()) .or_else(any_component_removed::<Animating>())
.or_else(any_component_removed::<Captured>()) .or_else(any_component_removed::<Captured>())
.or_else(any_component_removed::<Promoted>()) .or_else(any_component_removed::<Promoted>()),
), ),
set_models set_models
.run_if(resource_exists::<tweak::GameTweaks>) .run_if(resource_exists::<tweak::GameTweaks>)
.run_if( .run_if(
any_component_changed::<DisplayModel>() any_component_changed::<DisplayModel>()
.or_else(any_component_added::<DisplayModel>()) .or_else(any_component_added::<DisplayModel>())
.or_else(any_component_removed::<Animating>()) .or_else(any_component_removed::<Animating>()),
), ),
dissolve_animation.run_if(any_with_component::<Dissolving>), dissolve_animation.run_if(any_with_component::<Dissolving>),
capture_piece_start.run_if(any_component_added::<game::BeingCaptured>()), capture_piece_start.run_if(any_component_added::<game::BeingCaptured>()),
@ -121,19 +115,18 @@ impl Plugin for Display3dPlugin {
monitor_animations monitor_animations
.run_if(in_state(GameState::Play)) .run_if(in_state(GameState::Play))
.run_if(any_component_changed::<AnimationPlayer>()), .run_if(any_component_changed::<AnimationPlayer>()),
set_animation_player_speed set_animation_player_speed.run_if(
.run_if(any_component_added::<Animating>() any_component_added::<Animating>().or_else(resource_changed::<AnimationSpeed>),
.or_else(resource_changed::<AnimationSpeed>) ),
),
set_animation_speed set_animation_speed
.run_if(in_state(GameState::Play)) .run_if(in_state(GameState::Play))
.run_if(not(in_state(MenuState::On))) .run_if(not(in_state(MenuState::On)))
.run_if( .run_if(
just_pressed(KeyCode::Enter) just_pressed(KeyCode::Enter)
.or_else(just_pressed(MouseButton::Left)) .or_else(just_pressed(MouseButton::Left))
.or_else(just_released(KeyCode::Enter)) .or_else(just_released(KeyCode::Enter))
.or_else(just_released(MouseButton::Left)) .or_else(just_released(MouseButton::Left)),
) ),
), ),
) )
.add_systems( .add_systems(
@ -157,42 +150,32 @@ impl Plugin for Display3dPlugin {
fog_tweak.run_if(resource_exists::<tweak::GameTweaks>), fog_tweak.run_if(resource_exists::<tweak::GameTweaks>),
bloom_tweak.run_if(resource_exists::<tweak::GameTweaks>), bloom_tweak.run_if(resource_exists::<tweak::GameTweaks>),
set_models.run_if(resource_exists::<tweak::GameTweaks>), set_models.run_if(resource_exists::<tweak::GameTweaks>),
update_pieces.run_if(resource_exists::<tweak::GameTweaks>).after(set_models), update_pieces
.run_if(resource_exists::<tweak::GameTweaks>)
.after(set_models),
), ),
) )
.add_systems( .add_systems(
Update, Update,
( (setup_dissolve_materials.run_if(any_component_added::<Handle<StandardMaterial>>()),),
setup_dissolve_materials
.run_if(any_component_added::<Handle<StandardMaterial>>()),
)
) )
.add_systems( .add_systems(
OnEnter(GameState::Play), OnEnter(GameState::Play),
( (opening_animation
opening_animation .run_if(run_once())
.run_if(run_once()) .run_if(in_state(DisplayState::Display3d)),),
.run_if(in_state(DisplayState::Display3d)),
),
)
.add_systems(
OnEnter(GameState::Title),
(
fade_title_in,
fixup_shadows
)
) )
.add_systems(OnEnter(GameState::Title), (fade_title_in, fixup_shadows))
.add_systems(OnExit(GameState::Title), fade_title_out) .add_systems(OnExit(GameState::Title), fade_title_out)
.add_systems( .add_systems(
Update, Update,
( (
fade_title fade_title.run_if(any_with_component::<Fading>),
.run_if(any_with_component::<Fading>),
continue_title continue_title
.run_if(in_state(GameState::Title)) .run_if(in_state(GameState::Title))
.run_if(not(any_with_component::<Fading>)) .run_if(not(any_with_component::<Fading>))
.run_if(just_pressed(KeyCode::Enter).or_else(just_pressed(MouseButton::Left))), .run_if(just_pressed(KeyCode::Enter).or_else(just_pressed(MouseButton::Left))),
) ),
); );
} }
} }
@ -303,9 +286,7 @@ fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<Asset
// 3D objects root // 3D objects root
commands commands
.spawn(( .spawn((
SpatialBundle { SpatialBundle { ..default() },
..default()
},
Display3d, Display3d,
DisplayState::Display3d, DisplayState::Display3d,
)) ))
@ -313,91 +294,90 @@ fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<Asset
debug!("Intializing 3D Board!"); debug!("Intializing 3D Board!");
// Board // Board
parent parent.spawn((
.spawn(( Display3d,
DisplayState::Display3d,
game::BoardComponent,
DisplayModel("display3d_models_scenes_board"),
SceneBundle {
visibility: Visibility::Hidden,
..default()
},
Dissolvable {
start: 0.0,
duration: 12.0,
}, // Marks pieces as dissolving
));
// Hitboxes
game::tiles().for_each(|(index, tile)| {
let side = Board::side(index).expect("Spawn valid side");
parent.spawn((
DisplayState::Display3d,
Display3d, Display3d,
index,
tile,
PbrBundle {
mesh: assets.hitbox_shape.clone(),
material: assets.hitbox_material.clone(),
visibility: Visibility::Hidden,
transform: Transform::from_translation(board_translation(&index)),
..default()
},
side,
game::Selectable,
));
});
// Valid move indicators
game::tiles().for_each(|(index, _)| {
let side = Board::side(index).expect("Spawn valid side");
parent.spawn((
DisplayState::Display3d, DisplayState::Display3d,
game::BoardComponent, Display3d,
DisplayModel("display3d_models_scenes_board"), DisplayModel("display3d_models_scenes_valid_move"),
index,
SceneBundle { SceneBundle {
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
transform: Transform::from_translation(board_translation(&index)),
..default() ..default()
}, },
Dissolvable { side,
start: 0.0, game::ValidMove,
duration: 12.0,
}, // Marks pieces as dissolving
)); ));
});
// Hitboxes // Pieces
game::tiles().for_each(|(index, tile)| { let mut angle = 0.0;
let side = Board::side(index).expect("Spawn valid side"); board.pieces().iter().for_each(|(index, piece)| {
let side = Board::side(*index).expect("Spawn valid side");
parent.spawn((
DisplayState::Display3d,
Display3d,
index,
tile,
PbrBundle {
mesh: assets.hitbox_shape.clone(),
material: assets.hitbox_material.clone(),
visibility: Visibility::Hidden,
transform: Transform::from_translation(board_translation(&index)),
..default()
},
side,
game::Selectable,
));
});
// Valid move indicators // Rotates each piece 90 degrees offset from the previous piece
game::tiles().for_each(|(index, _)| { let rotation = Quat::from_rotation_y(angle);
let side = Board::side(index).expect("Spawn valid side"); let transform = Transform::default().with_rotation(rotation);
angle += std::f32::consts::PI / 2.0;
parent.spawn((
DisplayState::Display3d,
Display3d,
DisplayModel("display3d_models_scenes_valid_move"),
index,
SceneBundle {
visibility: Visibility::Hidden,
transform: Transform::from_translation(board_translation(&index)),
..default()
},
side,
game::ValidMove,
));
});
// Pieces parent.spawn((
let mut angle = 0.0; side,
board.pieces().iter().for_each(|(index, piece)| { DisplayState::Display3d,
let side = Board::side(*index).expect("Spawn valid side"); Display3d,
*piece,
// Rotates each piece 90 degrees offset from the previous piece *index,
let rotation = Quat::from_rotation_y(angle); SceneBundle {
let transform = Transform::default().with_rotation(rotation); visibility: Visibility::Hidden,
angle += std::f32::consts::PI / 2.0; transform,
..default()
parent.spawn(( },
side, DisplayModel(game::piece_model_key(*piece, side)),
DisplayState::Display3d, game::Selectable,
Display3d, Dissolvable {
*piece, start: 1.0,
*index, duration: 3.0,
SceneBundle { }, // Marks pieces as dissolving
visibility: Visibility::Hidden, ));
transform, });
..default()
},
DisplayModel(game::piece_model_key(*piece, side)),
game::Selectable,
Dissolvable {
start: 1.0,
duration: 3.0,
}, // Marks pieces as dissolving
));
});
}); });
} }
@ -561,25 +541,36 @@ fn set_models(
tweaks: Res<Assets<Tweaks>>, tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>, tweaks_file: Res<tweak::GameTweaks>,
) { ) {
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweakfile"); let tweak = tweaks
let assets_handle = tweak.get_handle::<Gltf>("display3d_models_assets_file").unwrap(); .get(tweaks_file.handle.clone())
.expect("Load tweakfile");
let assets_handle = tweak
.get_handle::<Gltf>("display3d_models_assets_file")
.unwrap();
let gltf = gltfs.get(assets_handle).expect("Load GLTF content"); let gltf = gltfs.get(assets_handle).expect("Load GLTF content");
query.iter_mut().for_each(|(entity, mut handle, DisplayModel(key))| { query
// Check if any children are animating .iter_mut()
if active_animation_players .for_each(|(entity, mut handle, DisplayModel(key))| {
.iter_many(children.iter_descendants(entity)) // Check if any children are animating
.count() > 0 { if active_animation_players
debug!("Piece {:?} is animating. Skipping...", entity); .iter_many(children.iter_descendants(entity))
} else { .count()
let scene = tweak.get::<String>(key).unwrap(); > 0
let new_handle = gltf.named_scenes.get(scene.as_str()).expect("Game board model"); {
if *new_handle != *handle { debug!("Piece {:?} is animating. Skipping...", entity);
debug!("Updating piece for {:?}", entity); } else {
*handle = new_handle.clone(); let scene = tweak.get::<String>(key).unwrap();
let new_handle = gltf
.named_scenes
.get(scene.as_str())
.expect("Game board model");
if *new_handle != *handle {
debug!("Updating piece for {:?}", entity);
*handle = new_handle.clone();
}
} }
} })
})
} }
/// Given a board index returns the Vec3 location in space /// Given a board index returns the Vec3 location in space
@ -694,7 +685,7 @@ fn select(
selected: Query<&BoardIndex, With<game::Selected>>, selected: Query<&BoardIndex, With<game::Selected>>,
state: Res<State<game::TurnState>>, state: Res<State<game::TurnState>>,
mut selections: EventWriter<game::Selection>, mut selections: EventWriter<game::Selection>,
mut moves: EventWriter<Move> mut moves: EventWriter<Move>,
) { ) {
match *piece { match *piece {
// Something is selected, so send an event saying such // Something is selected, so send an event saying such
@ -709,15 +700,13 @@ fn select(
}); });
} }
// Nothing selected, cancel the selection // Nothing selected, cancel the selection
PiecePointer(None) => { PiecePointer(None) => selected.iter().for_each(|board_index| {
selected.iter().for_each(|board_index| { moves.send(Move {
moves.send(Move { from: *board_index,
from: *board_index, to: Some(*board_index),
to: Some(*board_index), ..default()
..default() });
}); }),
})
},
} }
} }
@ -882,10 +871,7 @@ fn pick_up(
}); });
} }
fn de_select( fn de_select(query: Query<Entity, With<Selected>>, mut commands: Commands) {
query: Query<Entity, With<Selected>>,
mut commands: Commands,
) {
query.iter().for_each(|e| { query.iter().for_each(|e| {
commands.entity(e).remove::<Selected>(); commands.entity(e).remove::<Selected>();
}) })
@ -975,7 +961,8 @@ fn set_animation_speed(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>, mouse: Res<ButtonInput<MouseButton>>,
) { ) {
*animation_speed = if keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left) { *animation_speed = if keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left)
{
let tweak = tweaks let tweak = tweaks
.get(tweaks_file.handle.clone()) .get(tweaks_file.handle.clone())
.expect("Load tweakfile"); .expect("Load tweakfile");
@ -1003,14 +990,12 @@ fn should_switch_sides(
query: Query<&Side, (With<Camera>, With<Display3d>)>, query: Query<&Side, (With<Camera>, With<Display3d>)>,
state: Res<State<ai::PlayState>>, state: Res<State<ai::PlayState>>,
) -> bool { ) -> bool {
query.iter().all(|side| { query.iter().all(|side| match state.get() {
match state.get() { ai::PlayState::AiBogo => match side {
ai::PlayState::AiBogo => match side { Side::A => true,
Side::A => true, Side::B => false,
Side::B => false, },
} ai::PlayState::Human => true,
ai::PlayState::Human => true,
}
}) })
} }
@ -1034,11 +1019,11 @@ fn switch_sides(
game::TurnState(game::Side::B) => "display3d_models_animations_turn_b", game::TurnState(game::Side::B) => "display3d_models_animations_turn_b",
}; };
let animation_val = tweak.get::<String>(animation_key).unwrap(); let animation_val = tweak.get::<String>(animation_key).unwrap();
let animation = gltf.named_animations.get(animation_val.as_str()).expect("Camera Transition Animation"); let animation = gltf
player.start_with_transition( .named_animations
animation.clone(), .get(animation_val.as_str())
Duration::from_secs_f32(1.00), .expect("Camera Transition Animation");
); player.start_with_transition(animation.clone(), Duration::from_secs_f32(1.00));
*side = state.get().0; *side = state.get().0;
}); });
} }
@ -1049,7 +1034,7 @@ fn vantage_point(
state: Res<State<game::TurnState>>, state: Res<State<game::TurnState>>,
tweaks: Res<Assets<Tweaks>>, tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>, tweaks_file: Res<tweak::GameTweaks>,
mut up: Local<bool> mut up: Local<bool>,
) { ) {
let tweak = tweaks let tweak = tweaks
.get(tweaks_file.handle.clone()) .get(tweaks_file.handle.clone())
@ -1074,11 +1059,11 @@ fn vantage_point(
if animation_key != "" { if animation_key != "" {
let animation_val = tweak.get::<String>(animation_key).unwrap(); let animation_val = tweak.get::<String>(animation_key).unwrap();
let animation = gltf.named_animations.get(animation_val.as_str()).expect("Camera Transition Animation"); let animation = gltf
player.start_with_transition( .named_animations
animation.clone(), .get(animation_val.as_str())
Duration::from_secs_f32(1.00), .expect("Camera Transition Animation");
); player.start_with_transition(animation.clone(), Duration::from_secs_f32(1.00));
} }
}); });
} }
@ -1106,7 +1091,10 @@ impl MaterialExtension for DissolveExtension {
/// Sets up all pieces to have an associated "dissolve" material ready for capture /// Sets up all pieces to have an associated "dissolve" material ready for capture
fn setup_dissolve_materials( fn setup_dissolve_materials(
// All entities with materials are candidates for this procedure // All entities with materials are candidates for this procedure
events: Query<(Entity, &Handle<StandardMaterial>, &Name), (With<Parent>, Added<Handle<StandardMaterial>>)>, events: Query<
(Entity, &Handle<StandardMaterial>, &Name),
(With<Parent>, Added<Handle<StandardMaterial>>),
>,
// Only process newly created pieces (we do not delete pieces at runtime) // Only process newly created pieces (we do not delete pieces at runtime)
query: Query<&Dissolvable>, query: Query<&Dissolvable>,
// Children of pieces are the actual meshes that need materials // Children of pieces are the actual meshes that need materials
@ -1123,10 +1111,7 @@ fn setup_dissolve_materials(
.iter() .iter()
// Handle this entity (mesh) // Handle this entity (mesh)
.for_each(|(child, std_handle, name)| { .for_each(|(child, std_handle, name)| {
if let Some(dissolvable) = query if let Some(dissolvable) = query.iter_many(parents.iter_ancestors(child)).next() {
.iter_many(parents.iter_ancestors(child))
.next() {
debug!("Setting up dissolve material for {:?} {:?}", name, child); debug!("Setting up dissolve material for {:?} {:?}", name, child);
// Extension we will add to existing gltf-sourced materials // Extension we will add to existing gltf-sourced materials
@ -1185,7 +1170,9 @@ fn capture_piece_end(
.entity(entity) .entity(entity)
.insert(Dissolving::In(dissolvable.duration)) .insert(Dissolving::In(dissolvable.duration))
.remove::<BeingCaptured>() .remove::<BeingCaptured>()
.insert(Captured { epoch: board.current_epoch() - 1 }); .insert(Captured {
epoch: board.current_epoch() - 1,
});
} }
}); });
} }
@ -1436,21 +1423,27 @@ fn fade_title(
Fading::In(duration) => { Fading::In(duration) => {
// If we are fully saturated, remove dissolving // If we are fully saturated, remove dissolving
if bgc.0.a() >= 1.0 { if bgc.0.a() >= 1.0 {
commands.entity(e).remove::<Fading>().insert(Visibility::Inherited); commands
.entity(e)
.remove::<Fading>()
.insert(Visibility::Inherited);
} }
// Delta is simple // Delta is simple
time.delta_seconds() / duration time.delta_seconds() / duration
}, }
Fading::Out(duration) => { Fading::Out(duration) => {
// If we are fully saturated, remove dissolving // If we are fully saturated, remove dissolving
if bgc.0.a() <= 0.0 { if bgc.0.a() <= 0.0 {
commands.entity(e).remove::<Fading>().insert(Visibility::Hidden); commands
.entity(e)
.remove::<Fading>()
.insert(Visibility::Hidden);
} }
// Negative delta because we are fading out // Negative delta because we are fading out
-(time.delta_seconds() / duration) -(time.delta_seconds() / duration)
}, }
}; };
// Increment/decrement the alpha value // Increment/decrement the alpha value
let new_a = (bgc.0.a() + step).min(1.0).max(0.0); let new_a = (bgc.0.a() + step).min(1.0).max(0.0);

@ -15,7 +15,7 @@ impl Plugin for GamePlugin {
( (
manage_state_entities::<GameState>().run_if(state_changed::<GameState>), manage_state_entities::<GameState>().run_if(state_changed::<GameState>),
undo_move.run_if(just_pressed(KeyCode::KeyU)), undo_move.run_if(just_pressed(KeyCode::KeyU)),
) ),
) )
.add_systems( .add_systems(
Update, Update,
@ -29,19 +29,19 @@ impl Plugin for GamePlugin {
hide_valid_moves.run_if(any_component_removed::<Selected>()), hide_valid_moves.run_if(any_component_removed::<Selected>()),
manage_score.run_if(any_component_added::<Captured>()), manage_score.run_if(any_component_added::<Captured>()),
check_endgame.run_if(resource_changed::<Board>), check_endgame.run_if(resource_changed::<Board>),
).run_if(in_state(GameState::Play)), )
.run_if(in_state(GameState::Play)),
) )
.add_systems(Update, .add_systems(
Update,
assert_piece_consistency assert_piece_consistency
.run_if(in_state(GameState::Play)) .run_if(in_state(GameState::Play))
.run_if(resource_changed::<Score>) .run_if(resource_changed::<Score>),
) )
.add_systems(Update, reset_game.run_if(in_state(GameState::Restart))) .add_systems(Update, reset_game.run_if(in_state(GameState::Restart)))
.add_systems(OnEnter(GameState::Endgame), .add_systems(
( OnEnter(GameState::Endgame),
manage_score, (manage_score, set_endgame.after(manage_score)),
set_endgame.after(manage_score),
)
) )
.add_systems(OnExit(GameState::Endgame), clear_endgame) .add_systems(OnExit(GameState::Endgame), clear_endgame)
.add_systems( .add_systems(
@ -132,12 +132,16 @@ impl Piece {
} }
fn moves_at(&self, from: &BoardIndex) -> HashSet<BoardIndex> { fn moves_at(&self, from: &BoardIndex) -> HashSet<BoardIndex> {
self.moves().filter_map(|(x, y)| { self.moves()
let bi = (from.x as isize + x, from.y as isize + y); .filter_map(|(x, y)| {
// Check if this goes out of bounds, if so exclude from the list of possible moves let bi = (from.x as isize + x, from.y as isize + y);
(bi.0 <= 7 && bi.0 >= 0 && bi.1 <= 3 && bi.1 >= 0).then_some(BoardIndex { x: bi.0 as usize, y: bi.1 as usize }) // Check if this goes out of bounds, if so exclude from the list of possible moves
}) (bi.0 <= 7 && bi.0 >= 0 && bi.1 <= 3 && bi.1 >= 0).then_some(BoardIndex {
.collect() x: bi.0 as usize,
y: bi.1 as usize,
})
})
.collect()
} }
fn value(&self) -> usize { fn value(&self) -> usize {
@ -434,9 +438,7 @@ impl Board {
Some(from_piece) => { Some(from_piece) => {
let move_type = self.move_type(from, to); let move_type = self.move_type(from, to);
match move_type { match move_type {
MoveType::Invalid => { MoveType::Invalid => Err(GameError::InvalidMove),
Err(GameError::InvalidMove)
},
MoveType::Valid | MoveType::Capture | MoveType::Promotion(..) => { MoveType::Valid | MoveType::Capture | MoveType::Promotion(..) => {
// The current epoch is the last epoch + 1 // The current epoch is the last epoch + 1
let epoch = self.current_epoch(); let epoch = self.current_epoch();
@ -465,18 +467,15 @@ impl Board {
}); });
self.inner[to.y][to.x] = match move_type { self.inner[to.y][to.x] = match move_type {
MoveType::Promotion(_) => { MoveType::Promotion(_) => match (self.at(from), self.at(to)) {
match (self.at(from), self.at(to)) { (Some(Piece::Pawn), Some(Piece::Pawn)) => Some(Piece::Drone),
(Some(Piece::Pawn), Some(Piece::Pawn)) => { (Some(Piece::Pawn), Some(Piece::Drone))
Some(Piece::Drone) | (Some(Piece::Drone), Some(Piece::Pawn)) => Some(Piece::Queen),
}, _ => panic!(
(Some(Piece::Pawn), Some(Piece::Drone)) | (Some(Piece::Drone), Some(Piece::Pawn)) => { "Merges can only happen between pawn+pawn or pawn+drone!"
Some(Piece::Queen) ),
},
_ => panic!("Merges can only happen between pawn+pawn or pawn+drone!")
}
}, },
_ => Some(*from_piece) _ => Some(*from_piece),
}; };
self.inner[from.y][from.x] = None; self.inner[from.y][from.x] = None;
@ -554,100 +553,101 @@ impl Board {
} }
/// Determine given a piece, a to, and a from, what type of move this would be /// Determine given a piece, a to, and a from, what type of move this would be
pub(crate) fn move_type( pub(crate) fn move_type(&self, from: BoardIndex, to: BoardIndex) -> MoveType {
&self,
from: BoardIndex,
to: BoardIndex,
) -> MoveType {
self.line(from, to) self.line(from, to)
.all(|board_index| self.at(board_index).is_none()) .all(|board_index| self.at(board_index).is_none())
.then(|| { .then(|| {
self.at(from).map(|piece| { self.at(from).map(|piece| {
// Given that the side does not have a queen||drone // Given that the side does not have a queen||drone
// And the piece is a drone||pawn // And the piece is a drone||pawn
// We can do field promotions // We can do field promotions
let side = Board::side(from).expect("Piece has valid index"); let side = Board::side(from).expect("Piece has valid index");
let side_has_queen = self.on(side).iter().any(|(piece, _)| **piece == Piece::Queen); let side_has_queen = self
let side_has_drone = self.on(side).iter().any(|(piece, _)| **piece == Piece::Drone); .on(side)
.iter()
// Iterate over the piece's moves .any(|(piece, _)| **piece == Piece::Queen);
piece let side_has_drone = self
.moves_at(&from) .on(side)
.iter() .iter()
// Find if the given `to` move is one of those .any(|(piece, _)| **piece == Piece::Drone);
.find(|idx| to == **idx)
// Determine if this is valid/legal in this situation // Iterate over the piece's moves
.and_then(|_| { piece
let dest_at = self.at(to); .moves_at(&from)
let curr_side = Board::side(from).unwrap(); .iter()
let dest_side = Board::side(to).unwrap(); // Find if the given `to` move is one of those
.find(|idx| to == **idx)
match (curr_side, dest_side) { // Determine if this is valid/legal in this situation
(Side::A, Side::A) | (Side::B, Side::B) => { .and_then(|_| {
match dest_at { let dest_at = self.at(to);
// Cannot move on top of a friendly let curr_side = Board::side(from).unwrap();
Some(to_piece) => { let dest_side = Board::side(to).unwrap();
match (piece, to_piece) {
(Piece::Pawn, Piece::Pawn) => { match (curr_side, dest_side) {
(!side_has_drone).then_some(MoveType::Promotion(Piece::Drone)) (Side::A, Side::A) | (Side::B, Side::B) => {
} match dest_at {
(Piece::Drone, Piece::Pawn) | (Piece::Pawn, Piece::Drone) => { // Cannot move on top of a friendly
(!side_has_queen).then_some(MoveType::Promotion(Piece::Queen)) Some(to_piece) => match (piece, to_piece) {
}, (Piece::Pawn, Piece::Pawn) => (!side_has_drone)
_ => { .then_some(MoveType::Promotion(Piece::Drone)),
Some(MoveType::Invalid) (Piece::Drone, Piece::Pawn)
} | (Piece::Pawn, Piece::Drone) => (!side_has_queen)
} .then_some(MoveType::Promotion(Piece::Queen)),
}, _ => Some(MoveType::Invalid),
// Any other spot is valid },
None => { // Any other spot is valid
Some(MoveType::Valid) None => Some(MoveType::Valid),
} }
} }
} // Check for moving across the canal
// Check for moving across the canal (Side::A, Side::B) | (Side::B, Side::A) => {
(Side::A, Side::B) | (Side::B, Side::A) => { match dest_at {
match dest_at { Some(_) => Some(MoveType::Capture),
Some(_) => { None => {
Some(MoveType::Capture) debug!("Last move: {:?}", self.moves.last());
} // move is valid if it does not un-do the previous move for this piece
None => { match self.moves.last() {
debug!("Last move: {:?}", self.moves.last()); Some(previous) => {
// move is valid if it does not un-do the previous move for this piece let is_undo = previous.from == to
match self.moves.last() { && previous.to == Some(from);
Some(previous) => { (!is_undo).then_some(MoveType::Valid)
let is_undo = previous.from == to && previous.to == Some(from); }
(!is_undo).then_some(MoveType::Valid) // First move in the game, this is valid (and impossible)
} None => {
// First move in the game, this is valid (and impossible) // the move is valid
None => { Some(MoveType::Valid)
// the move is valid }
Some(MoveType::Valid)
} }
} }
} }
} }
} }
} })
})
}) })
}).flatten().flatten().or(Some(MoveType::Invalid)).unwrap() })
.flatten()
.flatten()
.or(Some(MoveType::Invalid))
.unwrap()
} }
/// Returns the possible moves the piece at this tile can make. /// Returns the possible moves the piece at this tile can make.
pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet<BoardIndex> { pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet<BoardIndex> {
if let Some(piece) = self.at(current_board_index) { if let Some(piece) = self.at(current_board_index) {
piece.moves_at(&current_board_index).iter().filter_map(|move_index| { piece
// Get the move type (or none if totally invalid) .moves_at(&current_board_index)
let result = self.move_type(current_board_index, *move_index); .iter()
match result { .filter_map(|move_index| {
MoveType::Invalid => None, // Get the move type (or none if totally invalid)
MoveType::Capture | MoveType::Promotion(..) | MoveType::Valid => { let result = self.move_type(current_board_index, *move_index);
Some(*move_index) match result {
}, MoveType::Invalid => None,
} MoveType::Capture | MoveType::Promotion(..) | MoveType::Valid => {
}) Some(*move_index)
.collect() }
}
})
.collect()
} else { } else {
HashSet::new() HashSet::new()
} }
@ -730,7 +730,11 @@ mod test {
(7, 2).into(), (7, 2).into(),
]); ]);
assert_eq!(expected, Piece::Queen.moves_at(&(4, 2).into()), "Generated queen moves"); assert_eq!(
expected,
Piece::Queen.moves_at(&(4, 2).into()),
"Generated queen moves"
);
assert_eq!(expected, given, "Basic queen moves"); assert_eq!(expected, given, "Basic queen moves");
} }
@ -753,7 +757,7 @@ mod test {
#[test] #[test]
fn moves_multi_step() { fn moves_multi_step() {
use super::*; use super::*;
let mut board = Board::from_ascii( let mut board = Board::from_ascii(
r#"........ r#"........
........ ........
@ -851,7 +855,8 @@ mod test {
// Pawn cannot merge with queen // Pawn cannot merge with queen
{ {
let given = board.valid_moves((2, 1).into()); let given = board.valid_moves((2, 1).into());
let expected: HashSet<BoardIndex> = HashSet::from([(3, 0).into(), (3, 2).into(), (1, 2).into()]); let expected: HashSet<BoardIndex> =
HashSet::from([(3, 0).into(), (3, 2).into(), (1, 2).into()]);
assert_eq!(expected, given, "Pawn cannot merge with queen"); assert_eq!(expected, given, "Pawn cannot merge with queen");
} }
} }
@ -870,18 +875,17 @@ mod test {
{ {
let given = board.valid_moves((4, 1).into()); let given = board.valid_moves((4, 1).into());
let expected: HashSet<BoardIndex> = let expected: HashSet<BoardIndex> = HashSet::from([
HashSet::from([ (1, 1).into(),
(1, 1).into(), (2, 1).into(),
(2, 1).into(), (2, 3).into(),
(2, 3).into(), (3, 0).into(),
(3, 0).into(), (3, 1).into(),
(3, 1).into(), (3, 2).into(),
(3, 2).into(), (4, 0).into(),
(4, 0).into(), (5, 0).into(),
(5, 0).into(), (5, 2).into(),
(5, 2).into(), ]);
]);
assert_eq!( assert_eq!(
expected, given, expected, given,
"Mostly blocked queen, moves include captures" "Mostly blocked queen, moves include captures"
@ -908,20 +912,13 @@ mod test {
{ {
let given = board.valid_moves((1, 0).into()); let given = board.valid_moves((1, 0).into());
let expected: HashSet<BoardIndex> = let expected: HashSet<BoardIndex> = HashSet::from([(2, 1).into()]);
HashSet::from([
(2, 1).into()
]);
assert_eq!(expected, given); assert_eq!(expected, given);
} }
{ {
let given = board.valid_moves((2, 0).into()); let given = board.valid_moves((2, 0).into());
let expected: HashSet<BoardIndex> = let expected: HashSet<BoardIndex> = HashSet::from([(2, 1).into(), (2, 2).into()]);
HashSet::from([
(2, 1).into(),
(2, 2).into(),
]);
assert_eq!(expected, given); assert_eq!(expected, given);
} }
} }
@ -950,7 +947,6 @@ mod test {
qqd....."#, qqd....."#,
); );
assert_eq!(expected.inner, board.inner); assert_eq!(expected.inner, board.inner);
} }
{ {
@ -966,7 +962,13 @@ mod test {
let given = Piece::Drone.moves_at(&(4, 1).into()); let given = Piece::Drone.moves_at(&(4, 1).into());
let expected: HashSet<BoardIndex> = HashSet::from([ let expected: HashSet<BoardIndex> = HashSet::from([
(4, 0).into(), (2, 1).into(), (3, 1).into(), (5, 1).into(), (6, 1).into(), (4, 2).into(), (4, 3).into(), (4, 0).into(),
(2, 1).into(),
(3, 1).into(),
(5, 1).into(),
(6, 1).into(),
(4, 2).into(),
(4, 3).into(),
]); ]);
assert_eq!(expected, given, "Drone moves at"); assert_eq!(expected, given, "Drone moves at");
@ -1024,7 +1026,8 @@ mod test {
{ {
// All normal moves for pawn are valid // All normal moves for pawn are valid
let given = board.valid_moves((3, 1).into()); let given = board.valid_moves((3, 1).into());
let expected: HashSet<BoardIndex> = HashSet::from([(2, 0).into(), (4, 0).into(), (2, 2).into(), (4, 2).into()]); let expected: HashSet<BoardIndex> =
HashSet::from([(2, 0).into(), (4, 0).into(), (2, 2).into(), (4, 2).into()]);
assert_eq!(expected, given); assert_eq!(expected, given);
// Pawn + Pawn on same side = Promotion // Pawn + Pawn on same side = Promotion
@ -1096,42 +1099,45 @@ pub(crate) fn update_board(
mut next_state: ResMut<NextState<TurnState>>, mut next_state: ResMut<NextState<TurnState>>,
) { ) {
// Each move event we get // Each move event we get
events.read().for_each(|Move { from, to, move_type, .. }| { events.read().for_each(
// Iterate over all pieces |Move {
pieces.iter_mut().for_each(|(entity, mut index)| { from,
// If the current index is the 'from' for the move to,
// All moves cover From -> To (captures and merges have to: None) move_type,
if *index == *from { ..
match to { }| {
// If we are moving on the board... // Iterate over all pieces
Some(to_idx) => { pieces.iter_mut().for_each(|(entity, mut index)| {
debug!("Moving piece {:?} {:?} -> {:?}", entity, from, to_idx); // If the current index is the 'from' for the move
// Update the piece's index // All moves cover From -> To (captures and merges have to: None)
*index = *to_idx; if *index == *from {
// Play audio sfx match to {
if !(*played) { // If we are moving on the board...
audio_events.send(audio::AudioEvent::PutDown); Some(to_idx) => {
audio_events.send(audio::AudioEvent::StopIdle); debug!("Moving piece {:?} {:?} -> {:?}", entity, from, to_idx);
*played = true; // Update the piece's index
} *index = *to_idx;
if *from != *to_idx { // Play audio sfx
match move_type { if !(*played) {
MoveType::Promotion(piece) => { audio_events.send(audio::AudioEvent::PutDown);
commands audio_events.send(audio::AudioEvent::StopIdle);
.entity(entity) *played = true;
.insert(*piece);
}
_ => ()
} }
if *from != *to_idx {
match move_type {
MoveType::Promotion(piece) => {
commands.entity(entity).insert(*piece);
}
_ => (),
}
let ns = !*curr_state.get(); let ns = !*curr_state.get();
debug!("Piece moved, switching sides: {:?}", ns); debug!("Piece moved, switching sides: {:?}", ns);
next_state.set(ns); next_state.set(ns);
}
} }
} // We are moving off the board (e.g,. capture or promotion/merge)
// We are moving off the board (e.g,. capture or promotion/merge) None => match move_type {
None => {
match move_type {
MoveType::Capture => { MoveType::Capture => {
debug!("Capturing piece {:?}", entity); debug!("Capturing piece {:?}", entity);
commands commands
@ -1139,26 +1145,26 @@ pub(crate) fn update_board(
.remove::<BoardIndex>() .remove::<BoardIndex>()
.insert(BeingCaptured); .insert(BeingCaptured);
audio_events.send(AudioEvent::Captured); audio_events.send(AudioEvent::Captured);
}, }
MoveType::Promotion(..) => { MoveType::Promotion(..) => {
commands commands
.entity(entity) .entity(entity)
.remove::<BoardIndex>() .remove::<BoardIndex>()
.insert((Promoted, Visibility::Hidden)); .insert((Promoted, Visibility::Hidden));
}, }
_ => { _ => {
panic!("How did you do this!?"); panic!("How did you do this!?");
} }
} },
} }
} }
} });
}); selected.iter().for_each(|entity| {
selected.iter().for_each(|entity| { debug!("De-selecting selected piece {:?}", entity);
debug!("De-selecting selected piece {:?}", entity); commands.entity(entity).remove::<Selected>();
commands.entity(entity).remove::<Selected>(); });
}); },
}); );
*played = false; *played = false;
} }
@ -1331,7 +1337,13 @@ fn clear_endgame(query: Query<Entity, With<Endgame>>, mut commands: Commands) {
/// * All captured pieces have their captured side preserved /// * All captured pieces have their captured side preserved
/// We can iterate over these pieces and calculate the score on the fly /// We can iterate over these pieces and calculate the score on the fly
fn manage_score( fn manage_score(
query: Query<(&Side, &Piece), (Or<(With<Captured>, With<BeingCaptured>)>, With<display3d::Display3d>)>, query: Query<
(&Side, &Piece),
(
Or<(With<Captured>, With<BeingCaptured>)>,
With<display3d::Display3d>,
),
>,
mut debug_info: ResMut<debug::DebugInfo>, mut debug_info: ResMut<debug::DebugInfo>,
mut score: ResMut<Score>, mut score: ResMut<Score>,
) { ) {
@ -1340,11 +1352,20 @@ fn manage_score(
score.captures_b = query.iter().filter(|(s, _)| **s == Side::A).count(); score.captures_b = query.iter().filter(|(s, _)| **s == Side::A).count();
// Calculate score based on the piece values // Calculate score based on the piece values
score.score_a = query.iter().filter(|(s, _)| **s == Side::B).fold(0, |acc, (_, piece)| acc + piece.value()); score.score_a = query
score.score_b = query.iter().filter(|(s, _)| **s == Side::A).fold(0, |acc, (_, piece)| acc + piece.value()); .iter()
.filter(|(s, _)| **s == Side::B)
.fold(0, |acc, (_, piece)| acc + piece.value());
score.score_b = query
.iter()
.filter(|(s, _)| **s == Side::A)
.fold(0, |acc, (_, piece)| acc + piece.value());
// Debug this for good measure // Debug this for good measure
debug_info.set("score".into(), format!("A:{}|B:{}", score.score_a, score.score_b)); debug_info.set(
"score".into(),
format!("A:{}|B:{}", score.score_a, score.score_b),
);
} }
pub(crate) fn set_side( pub(crate) fn set_side(
@ -1517,7 +1538,8 @@ fn reset_game(
.iter() .iter()
.zip(board.pieces().iter()) .zip(board.pieces().iter())
.for_each(|(e, (i, p))| { .for_each(|(e, (i, p))| {
commands.entity(e) commands
.entity(e)
.insert((*i, *p, Visibility::Inherited)) .insert((*i, *p, Visibility::Inherited))
.remove::<(BeingCaptured, Captured, Promoted)>(); .remove::<(BeingCaptured, Captured, Promoted)>();
}); });
@ -1538,9 +1560,12 @@ fn assert_piece_consistency(
captured: Query<Entity, (With<Piece>, With<Captured>)>, captured: Query<Entity, (With<Piece>, With<Captured>)>,
) { ) {
let active_count = active.iter().len(); let active_count = active.iter().len();
let being_captured_count = being_captured.iter().len(); let being_captured_count = being_captured.iter().len();
let captured_count = captured.iter().len(); let captured_count = captured.iter().len();
debug!("Active: {} | being captured: {} | captured: {}", active_count, being_captured_count, captured_count); debug!(
"Active: {} | being captured: {} | captured: {}",
active_count, being_captured_count, captured_count
);
let total_count = active_count + being_captured_count + captured_count; let total_count = active_count + being_captured_count + captured_count;
assert_eq!(total_count, 18, "Pieces does does not add up!"); assert_eq!(total_count, 18, "Pieces does does not add up!");
} }
@ -1557,46 +1582,39 @@ fn undo_move(
// Keep track of the current side in case we need to go back // Keep track of the current side in case we need to go back
let mut side = turn.get().0; let mut side = turn.get().0;
board board.undo_move().iter().for_each(
.undo_move() |Move {
.iter() epoch, from, to, ..
.for_each(|Move { epoch, from, to, .. }| { }| {
// If we have any moves to do, go back to the opposite side // If we have any moves to do, go back to the opposite side
if side == turn.get().0 { if side == turn.get().0 {
side = !turn.get().0; side = !turn.get().0;
} }
debug!("Reverting move {:?} {:?} -> {:?}", epoch, from, to); debug!("Reverting move {:?} {:?} -> {:?}", epoch, from, to);
match to { Some(to_idx) => { match to {
Some(to_idx) => {
debug!("Moving piece back from {:?}", to_idx); debug!("Moving piece back from {:?}", to_idx);
// Find piece currently at "to_idx" and update it's position to "from" // Find piece currently at "to_idx" and update it's position to "from"
active_pieces active_pieces
.iter_mut() .iter_mut()
.filter(|idx| { .filter(|idx| (idx.x, idx.y) == (to_idx.x, to_idx.y))
(idx.x, idx.y) == (to_idx.x, to_idx.y) .for_each(|mut idx| *idx = *from);
}) }
.for_each(|mut idx| {
*idx = *from
});
},
None => { None => {
captured_pieces captured_pieces
.iter() .iter()
.find_map(|(entity, captured)| { .find_map(|(entity, captured)| (captured.epoch == *epoch).then_some(entity))
(captured.epoch == *epoch).then_some(entity)
})
.iter() .iter()
.for_each(|entity| { .for_each(|entity| {
commands commands.entity(*entity).remove::<Captured>().insert(*from);
.entity(*entity)
.remove::<Captured>()
.insert(*from);
}); });
} }
} }
}); },
);
// Set the turn state (may be a no-op) // Set the turn state (may be a no-op)
next_turn.set(TurnState(side)); next_turn.set(TurnState(side));
} }

@ -53,7 +53,7 @@ impl Triangle {
pub(crate) fn intersects_aabb_3d(ray: &Ray3d, aabb: &Aabb, gt: &GlobalTransform) -> Option<Hit3d> { pub(crate) fn intersects_aabb_3d(ray: &Ray3d, aabb: &Aabb, gt: &GlobalTransform) -> Option<Hit3d> {
let world_to_model = gt.compute_matrix().inverse(); let world_to_model = gt.compute_matrix().inverse();
let ray_dir: Vec3A = world_to_model.transform_vector3(*ray.direction).into(); let ray_dir: Vec3A = world_to_model.transform_vector3(*ray.direction).into();
let ray_origin: Vec3A = world_to_model.transform_point3(ray.origin).into(); let ray_origin: Vec3A = world_to_model.transform_point3(ray.origin).into();
let t0 = (aabb.min() - ray_origin) / ray_dir; let t0 = (aabb.min() - ray_origin) / ray_dir;
@ -85,7 +85,9 @@ pub(crate) fn intersects_aabb_3d(ray: &Ray3d, aabb: &Aabb, gt: &GlobalTransform)
if t_max.z < hit_far { if t_max.z < hit_far {
hit_far = t_max.z; hit_far = t_max.z;
} }
Some(Hit3d { distance: (hit_near + hit_far) / 2.0 }) Some(Hit3d {
distance: (hit_near + hit_far) / 2.0,
})
} }
/// Heavily synthesized from these two resources: /// Heavily synthesized from these two resources:

@ -31,13 +31,12 @@ impl Plugin for IntroPlugin {
// Play intro manages playing the intro of each individual paragraph // Play intro manages playing the intro of each individual paragraph
// Runs every time the TextScroll component (managed by manage_scroll_text_animation) is updated // Runs every time the TextScroll component (managed by manage_scroll_text_animation) is updated
scroll_text.run_if(any_with_component::<ui::TextScroll>), scroll_text.run_if(any_with_component::<ui::TextScroll>),
set_text_animation_speed set_text_animation_speed.run_if(
.run_if( just_pressed(KeyCode::Enter)
just_pressed(KeyCode::Enter)
.or_else(just_pressed(MouseButton::Left)) .or_else(just_pressed(MouseButton::Left))
.or_else(just_released(KeyCode::Enter)) .or_else(just_released(KeyCode::Enter))
.or_else(just_released(MouseButton::Left)) .or_else(just_released(MouseButton::Left)),
) ),
) )
.run_if(in_state(GameState::Intro)), .run_if(in_state(GameState::Intro)),
); );
@ -63,7 +62,8 @@ fn set_text_animation_speed(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>, mouse: Res<ButtonInput<MouseButton>>,
) { ) {
*animation_speed = if keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left) { *animation_speed = if keys.just_pressed(KeyCode::Enter) || mouse.just_pressed(MouseButton::Left)
{
let tweak = tweaks let tweak = tweaks
.get(tweaks_file.handle.clone()) .get(tweaks_file.handle.clone())
.expect("Load tweakfile"); .expect("Load tweakfile");
@ -300,7 +300,9 @@ fn scroll_text(
*vis = Visibility::Inherited; *vis = Visibility::Inherited;
} }
text_scroll.stopwatch.tick(Duration::from_secs_f32(time.delta_seconds() * animation_speed.0)); text_scroll.stopwatch.tick(Duration::from_secs_f32(
time.delta_seconds() * animation_speed.0,
));
text_scroll.progress = { text_scroll.progress = {
// Update animation progress for this paragrpah // Update animation progress for this paragrpah

@ -65,21 +65,23 @@ fn loading(
.ids() .ids()
.filter(|id| matches!(id, AssetId::Uuid { .. })) .filter(|id| matches!(id, AssetId::Uuid { .. }))
.filter(|id| server.get_path(*id).is_some()) .filter(|id| server.get_path(*id).is_some())
.all(|id| { .all(|id| server.is_loaded_with_dependencies(id));
server.is_loaded_with_dependencies(id)
});
let g = (!gltfs.is_empty()) && gltfs.ids().all(|id| server.is_loaded_with_dependencies(id)); let g = (!gltfs.is_empty()) && gltfs.ids().all(|id| server.is_loaded_with_dependencies(id));
let t = (!tweaks.is_empty()) let t = (!tweaks.is_empty())
&& tweaks && tweaks
.ids() .ids()
.all(|id| server.is_loaded_with_dependencies(id)); .all(|id| server.is_loaded_with_dependencies(id));
let f = !fonts.is_empty() && let f = !fonts.is_empty()
fonts.ids() && fonts
.filter(|id| server.get_path(*id).is_some()) .ids()
.all(|id| server.is_loaded_with_dependencies(id)); .filter(|id| server.get_path(*id).is_some())
.all(|id| server.is_loaded_with_dependencies(id));
if s && g && t && f { if s && g && t && f {
debug!("Loading complete after {:?} seconds", time.elapsed_seconds()); debug!(
"Loading complete after {:?} seconds",
time.elapsed_seconds()
);
debug!("Starting game intro"); debug!("Starting game intro");
next_state.set(GameState::Intro) next_state.set(GameState::Intro)
} }

@ -22,7 +22,7 @@ use crate::prelude::*;
fn main() { fn main() {
if std::env::var_os("CARGO_MANIFEST_DIR").is_none() { if std::env::var_os("CARGO_MANIFEST_DIR").is_none() {
std::env::set_var("CARGO_MANIFEST_DIR", "."); std::env::set_current_dir(std::env::current_exe().unwrap().parent().unwrap());
} }
let mut app = App::new(); let mut app = App::new();
@ -37,7 +37,10 @@ fn main() {
.run_if(resource_changed::<State<tutorial::TutorialState>>), .run_if(resource_changed::<State<tutorial::TutorialState>>),
), ),
); );
#[cfg(not(target_os = "macos"))]
app.add_systems(Startup, set_window_icon); app.add_systems(Startup, set_window_icon);
app.add_plugins( app.add_plugins(
DefaultPlugins DefaultPlugins
.set(ImagePlugin::default_nearest()) .set(ImagePlugin::default_nearest())
@ -48,7 +51,8 @@ fn main() {
}), }),
..default() ..default()
}), }),
).add_systems(Update, handle_window.run_if(just_pressed(KeyCode::F11))); )
.add_systems(Update, handle_window.run_if(just_pressed(KeyCode::F11)));
app.add_plugins(credits::CreditsPlugin); app.add_plugins(credits::CreditsPlugin);
app.add_plugins(debug::DebugPlugin); app.add_plugins(debug::DebugPlugin);
@ -56,12 +60,13 @@ fn main() {
app.add_plugins(game::GamePlugin); app.add_plugins(game::GamePlugin);
app.add_plugins(loading::LoadingPlugin); app.add_plugins(loading::LoadingPlugin);
app.add_plugins(menu::MenuPlugin); app.add_plugins(menu::MenuPlugin);
app.add_plugins(audio::AudioPlugin);
app.add_plugins(ui::UiPlugin); app.add_plugins(ui::UiPlugin);
app.add_plugins(tweak::TweakPlugin); app.add_plugins(tweak::TweakPlugin);
app.add_plugins(intro::IntroPlugin); app.add_plugins(intro::IntroPlugin);
app.add_plugins(tutorial::TutorialPlugin); app.add_plugins(tutorial::TutorialPlugin);
app.add_plugins(ai::AiPlugin); app.add_plugins(ai::AiPlugin);
app.add_plugins(audio::AudioPlugin);
app.run(); app.run();
} }
@ -143,6 +148,7 @@ pub(crate) fn _any_component_added_or_changed<C: Component>(
!q.is_empty() !q.is_empty()
} }
#[cfg(not(target_os = "macos"))]
fn set_window_icon( fn set_window_icon(
// we have to use `NonSend` here // we have to use `NonSend` here
windows: NonSend<WinitWindows>, windows: NonSend<WinitWindows>,
@ -186,10 +192,7 @@ where
Box::new(move |buttons: Res<ButtonInput<T>>| -> bool { buttons.pressed(button) }) Box::new(move |buttons: Res<ButtonInput<T>>| -> bool { buttons.pressed(button) })
} }
fn handle_window(mut query: Query<&mut Window>) {
fn handle_window(
mut query: Query<&mut Window>
) {
use bevy::window::WindowMode; use bevy::window::WindowMode;
query.iter_mut().for_each(|mut window| { query.iter_mut().for_each(|mut window| {
@ -198,4 +201,4 @@ fn handle_window(
_ => WindowMode::BorderlessFullscreen, _ => WindowMode::BorderlessFullscreen,
} }
}) })
} }

@ -26,7 +26,10 @@ impl Plugin for MenuPlugin {
) )
.run_if(any_component_changed::<Interaction>()), .run_if(any_component_changed::<Interaction>()),
) )
.add_systems(Update, manage_ai_button.run_if(state_changed::<ai::PlayState>)) .add_systems(
Update,
manage_ai_button.run_if(state_changed::<ai::PlayState>),
)
.add_systems(Update, handle_escape.run_if(just_pressed(KeyCode::Escape))); .add_systems(Update, handle_escape.run_if(just_pressed(KeyCode::Escape)));
} }
} }
@ -386,11 +389,10 @@ fn handle_button_press<S: States + Clone + Component>(
}); });
} }
fn manage_ai_button( fn manage_ai_button(
curr: Res<State<ai::PlayState>>, curr: Res<State<ai::PlayState>>,
mut query: Query<(&mut ButtonAction<PlayState>, &Children)>, mut query: Query<(&mut ButtonAction<PlayState>, &Children)>,
mut texts: Query<&mut Text>, mut texts: Query<&mut Text>,
) { ) {
query.iter_mut().for_each(|(mut ba, children)| { query.iter_mut().for_each(|(mut ba, children)| {
ba.0 = match curr.get() { ba.0 = match curr.get() {
@ -403,7 +405,7 @@ fn manage_ai_button(
t.sections.iter_mut().for_each(|s| { t.sections.iter_mut().for_each(|s| {
s.value = match ba.0 { s.value = match ba.0 {
PlayState::AiBogo => "AI Opponent".into(), PlayState::AiBogo => "AI Opponent".into(),
PlayState::Human => "Human Opponent".into() PlayState::Human => "Human Opponent".into(),
} }
}); });
}); });

@ -1,4 +1,4 @@
pub(crate) use crate::{audio::AudioEvent, game::*, menu::*, tutorial::*, tweak::*, ai::*, *}; pub(crate) use crate::{ai::*, audio::AudioEvent, game::*, menu::*, tutorial::*, tweak::*, *};
pub(crate) use bevy::{ pub(crate) use bevy::{
animation::RepeatAnimation, animation::RepeatAnimation,

@ -20,18 +20,18 @@ impl Plugin for TutorialPlugin {
// tutorial must be running // tutorial must be running
.run_if(not(in_state(TutorialState::None))) .run_if(not(in_state(TutorialState::None)))
.run_if( .run_if(
// A piece changes sides // A piece changes sides
any_component_changed::<game::Side>() any_component_changed::<game::Side>()
// When a piece is selected, we // When a piece is selected, we
.or_else(any_component_added::<game::Selected>()) .or_else(any_component_added::<game::Selected>())
// A piece is de-selected // A piece is de-selected
.or_else(any_component_removed::<game::Selected>()) .or_else(any_component_removed::<game::Selected>())
// TEMP: The user hits 'enter' // TEMP: The user hits 'enter'
.or_else( .or_else(
just_pressed(KeyCode::Enter) just_pressed(KeyCode::Enter)
.or_else(just_pressed(MouseButton::Left)), .or_else(just_pressed(MouseButton::Left)),
), ),
), ),
), ),
) )
// Manage visible/hidden states // Manage visible/hidden states
@ -41,8 +41,8 @@ impl Plugin for TutorialPlugin {
manage_state_entities::<TutorialState>(), manage_state_entities::<TutorialState>(),
activate_tutorial_step, activate_tutorial_step,
) )
.run_if(not(in_state(GameState::Loading))) .run_if(not(in_state(GameState::Loading)))
.run_if(state_changed::<TutorialState>), .run_if(state_changed::<TutorialState>),
) )
.add_systems( .add_systems(
Update, Update,
@ -52,8 +52,9 @@ impl Plugin for TutorialPlugin {
.run_if(in_state(MenuState::Off)) .run_if(in_state(MenuState::Off))
.run_if(not(resource_exists::<TutorialStarted>)) .run_if(not(resource_exists::<TutorialStarted>))
.run_if(any_component_removed::<display3d::Dissolving>()), .run_if(any_component_removed::<display3d::Dissolving>()),
clear_tutorial_progress.run_if(on_event::<StateTransitionEvent<TutorialState>>()), clear_tutorial_progress
) .run_if(on_event::<StateTransitionEvent<TutorialState>>()),
),
); );
} }
} }
@ -514,7 +515,8 @@ fn step(
// Default, empty (tutorial doesn't always need to show something) // Default, empty (tutorial doesn't always need to show something)
else { else {
// When prompting to show a piece, find a random piece on this side and high-light it // When prompting to show a piece, find a random piece on this side and high-light it
all_pieces.iter() all_pieces
.iter()
.find_map(|(piece, board_index, side)| { .find_map(|(piece, board_index, side)| {
if *side == side_state.get().0 { if *side == side_state.get().0 {
if !queen_seen && *piece == Piece::Queen { if !queen_seen && *piece == Piece::Queen {
@ -576,7 +578,8 @@ fn start_tutorial_on_play(
mut next_state: ResMut<NextState<TutorialState>>, mut next_state: ResMut<NextState<TutorialState>>,
mut commands: Commands, mut commands: Commands,
) { ) {
if query.iter().len() == 0 && *state.get() == TutorialState::None && board.current_epoch() <= 1 { if query.iter().len() == 0 && *state.get() == TutorialState::None && board.current_epoch() <= 1
{
debug!("Starting intro tutorial!"); debug!("Starting intro tutorial!");
next_state.set(TutorialState::Intro); next_state.set(TutorialState::Intro);
commands.insert_resource(TutorialStarted); commands.insert_resource(TutorialStarted);
@ -587,5 +590,8 @@ fn clear_tutorial_progress(
mut events: EventReader<StateTransitionEvent<TutorialState>>, mut events: EventReader<StateTransitionEvent<TutorialState>>,
mut seen: ResMut<SeenStates>, mut seen: ResMut<SeenStates>,
) { ) {
events.read().filter(|StateTransitionEvent { after, .. }| *after == TutorialState::None).for_each(|_| seen.0.clear()); events
} .read()
.filter(|StateTransitionEvent { after, .. }| *after == TutorialState::None)
.for_each(|_| seen.0.clear());
}

@ -154,7 +154,7 @@ fn scale_ui(
fn init_prompts( fn init_prompts(
tweaks_file: Res<tweak::GameTweaks>, tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<tweak::Tweaks>>, tweaks: Res<Assets<tweak::Tweaks>>,
mut commands: Commands, mut commands: Commands,
) { ) {
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
let button_handle = tweak.get_handle::<Image>("buttons_image_resting").unwrap(); let button_handle = tweak.get_handle::<Image>("buttons_image_resting").unwrap();

Loading…
Cancel
Save