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`
[target.x86_64-apple-darwin]
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
"-Zthreads=0", # (Nightly) Use improved multithreading with the recommended amount of threads.
]
[target.aarch64-apple-darwin]
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
"-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
*.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
*.gltf 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
*.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]]
name = "bevy_fmod"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52cb5dfc9e27a6b8be8cabb6925ba1731e2a7f02bdc0bc774ba3b6b652613f46"
source = "git+https://github.com/Salzian/bevy_fmod.git?branch=main#bba24000626d251b1e1582d8b3863caa6dc701a6"
dependencies = [
"anyhow",
"bevy",

@ -1,11 +1,12 @@
[package]
name = "martian-chess"
description = "Martian Chess videogame"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[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"] }
serde = "1"
toml = { version = "0.8", features = ["parse"] }
@ -30,4 +31,4 @@ opt-level = "z"
strip = true
panic = "abort"
lto = true
codegen-units = 1
codegen-units = 1

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

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

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

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

@ -19,18 +19,11 @@ impl Plugin for Display3dPlugin {
color: Color::WHITE,
brightness: 100.0,
})
.insert_resource(PointLightShadowMap {
size: 512
})
.insert_resource(DirectionalLightShadowMap {
size: 512
})
.insert_resource(PointLightShadowMap { size: 512 })
.insert_resource(DirectionalLightShadowMap { size: 512 })
.add_systems(
OnExit(GameState::Loading),
(
fix_skybox,
initialize.after(fix_skybox),
),
(fix_skybox, initialize.after(fix_skybox)),
)
// Systems related to color and camera
.add_systems(
@ -70,7 +63,7 @@ impl Plugin for Display3dPlugin {
pick_up.run_if(any_component_added::<game::Selected>()),
de_select.run_if(just_pressed(MouseButton::Right)),
put_down.run_if(any_component_removed::<game::Selected>()),
)
),
)
.add_systems(
Update,
@ -86,8 +79,9 @@ impl Plugin for Display3dPlugin {
switch_sides
.run_if(in_state(GameState::Play))
.run_if(state_changed::<game::TurnState>)
.run_if(any_component_added::<Selected>()
.or_else(any_component_removed::<Selected>())
.run_if(
any_component_added::<Selected>()
.or_else(any_component_removed::<Selected>()),
)
.run_if(should_switch_sides),
// 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_removed::<Animating>())
.or_else(any_component_removed::<Captured>())
.or_else(any_component_removed::<Promoted>())
.or_else(any_component_removed::<Promoted>()),
),
set_models
.run_if(resource_exists::<tweak::GameTweaks>)
.run_if(
any_component_changed::<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>),
capture_piece_start.run_if(any_component_added::<game::BeingCaptured>()),
@ -121,19 +115,18 @@ impl Plugin for Display3dPlugin {
monitor_animations
.run_if(in_state(GameState::Play))
.run_if(any_component_changed::<AnimationPlayer>()),
set_animation_player_speed
.run_if(any_component_added::<Animating>()
.or_else(resource_changed::<AnimationSpeed>)
),
set_animation_player_speed.run_if(
any_component_added::<Animating>().or_else(resource_changed::<AnimationSpeed>),
),
set_animation_speed
.run_if(in_state(GameState::Play))
.run_if(not(in_state(MenuState::On)))
.run_if(
just_pressed(KeyCode::Enter)
.or_else(just_pressed(MouseButton::Left))
.or_else(just_released(KeyCode::Enter))
.or_else(just_released(MouseButton::Left))
)
.or_else(just_pressed(MouseButton::Left))
.or_else(just_released(KeyCode::Enter))
.or_else(just_released(MouseButton::Left)),
),
),
)
.add_systems(
@ -157,42 +150,32 @@ impl Plugin for Display3dPlugin {
fog_tweak.run_if(resource_exists::<tweak::GameTweaks>),
bloom_tweak.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(
Update,
(
setup_dissolve_materials
.run_if(any_component_added::<Handle<StandardMaterial>>()),
)
(setup_dissolve_materials.run_if(any_component_added::<Handle<StandardMaterial>>()),),
)
.add_systems(
OnEnter(GameState::Play),
(
opening_animation
.run_if(run_once())
.run_if(in_state(DisplayState::Display3d)),
),
)
.add_systems(
OnEnter(GameState::Title),
(
fade_title_in,
fixup_shadows
)
(opening_animation
.run_if(run_once())
.run_if(in_state(DisplayState::Display3d)),),
)
.add_systems(OnEnter(GameState::Title), (fade_title_in, fixup_shadows))
.add_systems(OnExit(GameState::Title), fade_title_out)
.add_systems(
Update,
(
fade_title
.run_if(any_with_component::<Fading>),
fade_title.run_if(any_with_component::<Fading>),
continue_title
.run_if(in_state(GameState::Title))
.run_if(not(any_with_component::<Fading>))
.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
commands
.spawn((
SpatialBundle {
..default()
},
SpatialBundle { ..default() },
Display3d,
DisplayState::Display3d,
))
@ -313,91 +294,90 @@ fn initialize(mut commands: Commands, board: Res<game::Board>, assets: Res<Asset
debug!("Intializing 3D Board!");
// Board
parent
.spawn((
parent.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,
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,
game::BoardComponent,
DisplayModel("display3d_models_scenes_board"),
Display3d,
DisplayModel("display3d_models_scenes_valid_move"),
index,
SceneBundle {
visibility: Visibility::Hidden,
transform: Transform::from_translation(board_translation(&index)),
..default()
},
Dissolvable {
start: 0.0,
duration: 12.0,
}, // Marks pieces as dissolving
side,
game::ValidMove,
));
});
// Hitboxes
game::tiles().for_each(|(index, tile)| {
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,
));
});
// Pieces
let mut angle = 0.0;
board.pieces().iter().for_each(|(index, piece)| {
let side = Board::side(*index).expect("Spawn valid side");
// Valid move indicators
game::tiles().for_each(|(index, _)| {
let side = Board::side(index).expect("Spawn valid side");
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,
));
});
// Rotates each piece 90 degrees offset from the previous piece
let rotation = Quat::from_rotation_y(angle);
let transform = Transform::default().with_rotation(rotation);
angle += std::f32::consts::PI / 2.0;
// Pieces
let mut angle = 0.0;
board.pieces().iter().for_each(|(index, piece)| {
let side = Board::side(*index).expect("Spawn valid side");
// Rotates each piece 90 degrees offset from the previous piece
let rotation = Quat::from_rotation_y(angle);
let transform = Transform::default().with_rotation(rotation);
angle += std::f32::consts::PI / 2.0;
parent.spawn((
side,
DisplayState::Display3d,
Display3d,
*piece,
*index,
SceneBundle {
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
));
});
parent.spawn((
side,
DisplayState::Display3d,
Display3d,
*piece,
*index,
SceneBundle {
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_file: Res<tweak::GameTweaks>,
) {
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweakfile");
let assets_handle = tweak.get_handle::<Gltf>("display3d_models_assets_file").unwrap();
let tweak = tweaks
.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");
query.iter_mut().for_each(|(entity, mut handle, DisplayModel(key))| {
// Check if any children are animating
if active_animation_players
.iter_many(children.iter_descendants(entity))
.count() > 0 {
debug!("Piece {:?} is animating. Skipping...", entity);
} else {
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();
query
.iter_mut()
.for_each(|(entity, mut handle, DisplayModel(key))| {
// Check if any children are animating
if active_animation_players
.iter_many(children.iter_descendants(entity))
.count()
> 0
{
debug!("Piece {:?} is animating. Skipping...", entity);
} else {
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
@ -694,7 +685,7 @@ fn select(
selected: Query<&BoardIndex, With<game::Selected>>,
state: Res<State<game::TurnState>>,
mut selections: EventWriter<game::Selection>,
mut moves: EventWriter<Move>
mut moves: EventWriter<Move>,
) {
match *piece {
// Something is selected, so send an event saying such
@ -709,15 +700,13 @@ fn select(
});
}
// Nothing selected, cancel the selection
PiecePointer(None) => {
selected.iter().for_each(|board_index| {
moves.send(Move {
from: *board_index,
to: Some(*board_index),
..default()
});
})
},
PiecePointer(None) => selected.iter().for_each(|board_index| {
moves.send(Move {
from: *board_index,
to: Some(*board_index),
..default()
});
}),
}
}
@ -882,10 +871,7 @@ fn pick_up(
});
}
fn de_select(
query: Query<Entity, With<Selected>>,
mut commands: Commands,
) {
fn de_select(query: Query<Entity, With<Selected>>, mut commands: Commands) {
query.iter().for_each(|e| {
commands.entity(e).remove::<Selected>();
})
@ -975,7 +961,8 @@ fn set_animation_speed(
keys: Res<ButtonInput<KeyCode>>,
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
.get(tweaks_file.handle.clone())
.expect("Load tweakfile");
@ -1003,14 +990,12 @@ fn should_switch_sides(
query: Query<&Side, (With<Camera>, With<Display3d>)>,
state: Res<State<ai::PlayState>>,
) -> bool {
query.iter().all(|side| {
match state.get() {
ai::PlayState::AiBogo => match side {
Side::A => true,
Side::B => false,
}
ai::PlayState::Human => true,
}
query.iter().all(|side| match state.get() {
ai::PlayState::AiBogo => match side {
Side::A => true,
Side::B => false,
},
ai::PlayState::Human => true,
})
}
@ -1034,11 +1019,11 @@ fn switch_sides(
game::TurnState(game::Side::B) => "display3d_models_animations_turn_b",
};
let animation_val = tweak.get::<String>(animation_key).unwrap();
let animation = gltf.named_animations.get(animation_val.as_str()).expect("Camera Transition Animation");
player.start_with_transition(
animation.clone(),
Duration::from_secs_f32(1.00),
);
let animation = gltf
.named_animations
.get(animation_val.as_str())
.expect("Camera Transition Animation");
player.start_with_transition(animation.clone(), Duration::from_secs_f32(1.00));
*side = state.get().0;
});
}
@ -1049,7 +1034,7 @@ fn vantage_point(
state: Res<State<game::TurnState>>,
tweaks: Res<Assets<Tweaks>>,
tweaks_file: Res<tweak::GameTweaks>,
mut up: Local<bool>
mut up: Local<bool>,
) {
let tweak = tweaks
.get(tweaks_file.handle.clone())
@ -1074,11 +1059,11 @@ fn vantage_point(
if animation_key != "" {
let animation_val = tweak.get::<String>(animation_key).unwrap();
let animation = gltf.named_animations.get(animation_val.as_str()).expect("Camera Transition Animation");
player.start_with_transition(
animation.clone(),
Duration::from_secs_f32(1.00),
);
let animation = gltf
.named_animations
.get(animation_val.as_str())
.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
fn setup_dissolve_materials(
// 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)
query: Query<&Dissolvable>,
// Children of pieces are the actual meshes that need materials
@ -1123,10 +1111,7 @@ fn setup_dissolve_materials(
.iter()
// Handle this entity (mesh)
.for_each(|(child, std_handle, name)| {
if let Some(dissolvable) = query
.iter_many(parents.iter_ancestors(child))
.next() {
if let Some(dissolvable) = query.iter_many(parents.iter_ancestors(child)).next() {
debug!("Setting up dissolve material for {:?} {:?}", name, child);
// Extension we will add to existing gltf-sourced materials
@ -1185,7 +1170,9 @@ fn capture_piece_end(
.entity(entity)
.insert(Dissolving::In(dissolvable.duration))
.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) => {
// If we are fully saturated, remove dissolving
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
time.delta_seconds() / duration
},
}
Fading::Out(duration) => {
// If we are fully saturated, remove dissolving
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
-(time.delta_seconds() / duration)
},
}
};
// Increment/decrement the alpha value
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>),
undo_move.run_if(just_pressed(KeyCode::KeyU)),
)
),
)
.add_systems(
Update,
@ -29,19 +29,19 @@ impl Plugin for GamePlugin {
hide_valid_moves.run_if(any_component_removed::<Selected>()),
manage_score.run_if(any_component_added::<Captured>()),
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
.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(OnEnter(GameState::Endgame),
(
manage_score,
set_endgame.after(manage_score),
)
.add_systems(
OnEnter(GameState::Endgame),
(manage_score, set_endgame.after(manage_score)),
)
.add_systems(OnExit(GameState::Endgame), clear_endgame)
.add_systems(
@ -132,12 +132,16 @@ impl Piece {
}
fn moves_at(&self, from: &BoardIndex) -> HashSet<BoardIndex> {
self.moves().filter_map(|(x, y)| {
let bi = (from.x as isize + x, from.y as isize + y);
// 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 { x: bi.0 as usize, y: bi.1 as usize })
})
.collect()
self.moves()
.filter_map(|(x, y)| {
let bi = (from.x as isize + x, from.y as isize + y);
// 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 {
x: bi.0 as usize,
y: bi.1 as usize,
})
})
.collect()
}
fn value(&self) -> usize {
@ -434,9 +438,7 @@ impl Board {
Some(from_piece) => {
let move_type = self.move_type(from, to);
match move_type {
MoveType::Invalid => {
Err(GameError::InvalidMove)
},
MoveType::Invalid => Err(GameError::InvalidMove),
MoveType::Valid | MoveType::Capture | MoveType::Promotion(..) => {
// The current epoch is the last epoch + 1
let epoch = self.current_epoch();
@ -465,18 +467,15 @@ impl Board {
});
self.inner[to.y][to.x] = match move_type {
MoveType::Promotion(_) => {
match (self.at(from), self.at(to)) {
(Some(Piece::Pawn), Some(Piece::Pawn)) => {
Some(Piece::Drone)
},
(Some(Piece::Pawn), Some(Piece::Drone)) | (Some(Piece::Drone), Some(Piece::Pawn)) => {
Some(Piece::Queen)
},
_ => panic!("Merges can only happen between pawn+pawn or pawn+drone!")
}
MoveType::Promotion(_) => match (self.at(from), self.at(to)) {
(Some(Piece::Pawn), Some(Piece::Pawn)) => Some(Piece::Drone),
(Some(Piece::Pawn), Some(Piece::Drone))
| (Some(Piece::Drone), Some(Piece::Pawn)) => 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;
@ -554,100 +553,101 @@ impl Board {
}
/// Determine given a piece, a to, and a from, what type of move this would be
pub(crate) fn move_type(
&self,
from: BoardIndex,
to: BoardIndex,
) -> MoveType {
pub(crate) fn move_type(&self, from: BoardIndex, to: BoardIndex) -> MoveType {
self.line(from, to)
.all(|board_index| self.at(board_index).is_none())
.then(|| {
self.at(from).map(|piece| {
// Given that the side does not have a queen||drone
// And the piece is a drone||pawn
// We can do field promotions
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_drone = self.on(side).iter().any(|(piece, _)| **piece == Piece::Drone);
// Iterate over the piece's moves
piece
.moves_at(&from)
.iter()
// Find if the given `to` move is one of those
.find(|idx| to == **idx)
// Determine if this is valid/legal in this situation
.and_then(|_| {
let dest_at = self.at(to);
let curr_side = Board::side(from).unwrap();
let dest_side = Board::side(to).unwrap();
match (curr_side, dest_side) {
(Side::A, Side::A) | (Side::B, Side::B) => {
match dest_at {
// Cannot move on top of a friendly
Some(to_piece) => {
match (piece, to_piece) {
(Piece::Pawn, Piece::Pawn) => {
(!side_has_drone).then_some(MoveType::Promotion(Piece::Drone))
}
(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 => {
Some(MoveType::Valid)
self.at(from).map(|piece| {
// Given that the side does not have a queen||drone
// And the piece is a drone||pawn
// We can do field promotions
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_drone = self
.on(side)
.iter()
.any(|(piece, _)| **piece == Piece::Drone);
// Iterate over the piece's moves
piece
.moves_at(&from)
.iter()
// Find if the given `to` move is one of those
.find(|idx| to == **idx)
// Determine if this is valid/legal in this situation
.and_then(|_| {
let dest_at = self.at(to);
let curr_side = Board::side(from).unwrap();
let dest_side = Board::side(to).unwrap();
match (curr_side, dest_side) {
(Side::A, Side::A) | (Side::B, Side::B) => {
match dest_at {
// Cannot move on top of a friendly
Some(to_piece) => match (piece, to_piece) {
(Piece::Pawn, Piece::Pawn) => (!side_has_drone)
.then_some(MoveType::Promotion(Piece::Drone)),
(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 => Some(MoveType::Valid),
}
}
}
// Check for moving across the canal
(Side::A, Side::B) | (Side::B, Side::A) => {
match dest_at {
Some(_) => {
Some(MoveType::Capture)
}
None => {
debug!("Last move: {:?}", self.moves.last());
// move is valid if it does not un-do the previous move for this piece
match self.moves.last() {
Some(previous) => {
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 => {
// the move is valid
Some(MoveType::Valid)
// Check for moving across the canal
(Side::A, Side::B) | (Side::B, Side::A) => {
match dest_at {
Some(_) => Some(MoveType::Capture),
None => {
debug!("Last move: {:?}", self.moves.last());
// move is valid if it does not un-do the previous move for this piece
match self.moves.last() {
Some(previous) => {
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 => {
// 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.
pub(crate) fn valid_moves(&self, current_board_index: BoardIndex) -> HashSet<BoardIndex> {
if let Some(piece) = self.at(current_board_index) {
piece.moves_at(&current_board_index).iter().filter_map(|move_index| {
// Get the move type (or none if totally invalid)
let result = self.move_type(current_board_index, *move_index);
match result {
MoveType::Invalid => None,
MoveType::Capture | MoveType::Promotion(..) | MoveType::Valid => {
Some(*move_index)
},
}
})
.collect()
piece
.moves_at(&current_board_index)
.iter()
.filter_map(|move_index| {
// Get the move type (or none if totally invalid)
let result = self.move_type(current_board_index, *move_index);
match result {
MoveType::Invalid => None,
MoveType::Capture | MoveType::Promotion(..) | MoveType::Valid => {
Some(*move_index)
}
}
})
.collect()
} else {
HashSet::new()
}
@ -730,7 +730,11 @@ mod test {
(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");
}
@ -753,7 +757,7 @@ mod test {
#[test]
fn moves_multi_step() {
use super::*;
let mut board = Board::from_ascii(
r#"........
........
@ -851,7 +855,8 @@ mod test {
// Pawn cannot merge with queen
{
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");
}
}
@ -870,18 +875,17 @@ mod test {
{
let given = board.valid_moves((4, 1).into());
let expected: HashSet<BoardIndex> =
HashSet::from([
(1, 1).into(),
(2, 1).into(),
(2, 3).into(),
(3, 0).into(),
(3, 1).into(),
(3, 2).into(),
(4, 0).into(),
(5, 0).into(),
(5, 2).into(),
]);
let expected: HashSet<BoardIndex> = HashSet::from([
(1, 1).into(),
(2, 1).into(),
(2, 3).into(),
(3, 0).into(),
(3, 1).into(),
(3, 2).into(),
(4, 0).into(),
(5, 0).into(),
(5, 2).into(),
]);
assert_eq!(
expected, given,
"Mostly blocked queen, moves include captures"
@ -908,20 +912,13 @@ mod test {
{
let given = board.valid_moves((1, 0).into());
let expected: HashSet<BoardIndex> =
HashSet::from([
(2, 1).into()
]);
let expected: HashSet<BoardIndex> = HashSet::from([(2, 1).into()]);
assert_eq!(expected, given);
}
{
let given = board.valid_moves((2, 0).into());
let expected: HashSet<BoardIndex> =
HashSet::from([
(2, 1).into(),
(2, 2).into(),
]);
let expected: HashSet<BoardIndex> = HashSet::from([(2, 1).into(), (2, 2).into()]);
assert_eq!(expected, given);
}
}
@ -950,7 +947,6 @@ mod test {
qqd....."#,
);
assert_eq!(expected.inner, board.inner);
}
{
@ -966,7 +962,13 @@ mod test {
let given = Piece::Drone.moves_at(&(4, 1).into());
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");
@ -1024,7 +1026,8 @@ mod test {
{
// All normal moves for pawn are valid
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);
// Pawn + Pawn on same side = Promotion
@ -1096,42 +1099,45 @@ pub(crate) fn update_board(
mut next_state: ResMut<NextState<TurnState>>,
) {
// Each move event we get
events.read().for_each(|Move { from, to, move_type, .. }| {
// Iterate over all pieces
pieces.iter_mut().for_each(|(entity, mut index)| {
// If the current index is the 'from' for the move
// All moves cover From -> To (captures and merges have to: None)
if *index == *from {
match to {
// If we are moving on the board...
Some(to_idx) => {
debug!("Moving piece {:?} {:?} -> {:?}", entity, from, to_idx);
// Update the piece's index
*index = *to_idx;
// Play audio sfx
if !(*played) {
audio_events.send(audio::AudioEvent::PutDown);
audio_events.send(audio::AudioEvent::StopIdle);
*played = true;
}
if *from != *to_idx {
match move_type {
MoveType::Promotion(piece) => {
commands
.entity(entity)
.insert(*piece);
}
_ => ()
events.read().for_each(
|Move {
from,
to,
move_type,
..
}| {
// Iterate over all pieces
pieces.iter_mut().for_each(|(entity, mut index)| {
// If the current index is the 'from' for the move
// All moves cover From -> To (captures and merges have to: None)
if *index == *from {
match to {
// If we are moving on the board...
Some(to_idx) => {
debug!("Moving piece {:?} {:?} -> {:?}", entity, from, to_idx);
// Update the piece's index
*index = *to_idx;
// Play audio sfx
if !(*played) {
audio_events.send(audio::AudioEvent::PutDown);
audio_events.send(audio::AudioEvent::StopIdle);
*played = true;
}
if *from != *to_idx {
match move_type {
MoveType::Promotion(piece) => {
commands.entity(entity).insert(*piece);
}
_ => (),
}
let ns = !*curr_state.get();
debug!("Piece moved, switching sides: {:?}", ns);
next_state.set(ns);
let ns = !*curr_state.get();
debug!("Piece moved, switching sides: {:?}", ns);
next_state.set(ns);
}
}
}
// We are moving off the board (e.g,. capture or promotion/merge)
None => {
match move_type {
// We are moving off the board (e.g,. capture or promotion/merge)
None => match move_type {
MoveType::Capture => {
debug!("Capturing piece {:?}", entity);
commands
@ -1139,26 +1145,26 @@ pub(crate) fn update_board(
.remove::<BoardIndex>()
.insert(BeingCaptured);
audio_events.send(AudioEvent::Captured);
},
}
MoveType::Promotion(..) => {
commands
.entity(entity)
.remove::<BoardIndex>()
.insert((Promoted, Visibility::Hidden));
},
}
_ => {
panic!("How did you do this!?");
}
}
},
}
}
}
});
selected.iter().for_each(|entity| {
debug!("De-selecting selected piece {:?}", entity);
commands.entity(entity).remove::<Selected>();
});
});
});
selected.iter().for_each(|entity| {
debug!("De-selecting selected piece {:?}", entity);
commands.entity(entity).remove::<Selected>();
});
},
);
*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
/// We can iterate over these pieces and calculate the score on the fly
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 score: ResMut<Score>,
) {
@ -1340,11 +1352,20 @@ fn manage_score(
score.captures_b = query.iter().filter(|(s, _)| **s == Side::A).count();
// 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_b = query.iter().filter(|(s, _)| **s == Side::A).fold(0, |acc, (_, piece)| acc + piece.value());
score.score_a = query
.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_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(
@ -1517,7 +1538,8 @@ fn reset_game(
.iter()
.zip(board.pieces().iter())
.for_each(|(e, (i, p))| {
commands.entity(e)
commands
.entity(e)
.insert((*i, *p, Visibility::Inherited))
.remove::<(BeingCaptured, Captured, Promoted)>();
});
@ -1538,9 +1560,12 @@ fn assert_piece_consistency(
captured: Query<Entity, (With<Piece>, With<Captured>)>,
) {
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();
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;
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
let mut side = turn.get().0;
board
.undo_move()
.iter()
.for_each(|Move { epoch, from, to, .. }| {
board.undo_move().iter().for_each(
|Move {
epoch, from, to, ..
}| {
// If we have any moves to do, go back to the opposite side
if side == turn.get().0 {
side = !turn.get().0;
}
debug!("Reverting move {:?} {:?} -> {:?}", epoch, from, to);
match to { Some(to_idx) => {
match to {
Some(to_idx) => {
debug!("Moving piece back from {:?}", to_idx);
// Find piece currently at "to_idx" and update it's position to "from"
active_pieces
.iter_mut()
.filter(|idx| {
(idx.x, idx.y) == (to_idx.x, to_idx.y)
})
.for_each(|mut idx| {
*idx = *from
});
},
.filter(|idx| (idx.x, idx.y) == (to_idx.x, to_idx.y))
.for_each(|mut idx| *idx = *from);
}
None => {
captured_pieces
.iter()
.find_map(|(entity, captured)| {
(captured.epoch == *epoch).then_some(entity)
})
.find_map(|(entity, captured)| (captured.epoch == *epoch).then_some(entity))
.iter()
.for_each(|entity| {
commands
.entity(*entity)
.remove::<Captured>()
.insert(*from);
commands.entity(*entity).remove::<Captured>().insert(*from);
});
}
}
});
},
);
// Set the turn state (may be a no-op)
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> {
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 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 {
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:

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

@ -65,21 +65,23 @@ fn loading(
.ids()
.filter(|id| matches!(id, AssetId::Uuid { .. }))
.filter(|id| server.get_path(*id).is_some())
.all(|id| {
server.is_loaded_with_dependencies(id)
});
.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())
&& tweaks
.ids()
.all(|id| server.is_loaded_with_dependencies(id));
let f = !fonts.is_empty() &&
fonts.ids()
.filter(|id| server.get_path(*id).is_some())
.all(|id| server.is_loaded_with_dependencies(id));
let f = !fonts.is_empty()
&& fonts
.ids()
.filter(|id| server.get_path(*id).is_some())
.all(|id| server.is_loaded_with_dependencies(id));
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");
next_state.set(GameState::Intro)
}

@ -22,7 +22,7 @@ use crate::prelude::*;
fn main() {
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();
@ -37,7 +37,10 @@ fn main() {
.run_if(resource_changed::<State<tutorial::TutorialState>>),
),
);
#[cfg(not(target_os = "macos"))]
app.add_systems(Startup, set_window_icon);
app.add_plugins(
DefaultPlugins
.set(ImagePlugin::default_nearest())
@ -48,7 +51,8 @@ fn main() {
}),
..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(debug::DebugPlugin);
@ -56,12 +60,13 @@ fn main() {
app.add_plugins(game::GamePlugin);
app.add_plugins(loading::LoadingPlugin);
app.add_plugins(menu::MenuPlugin);
app.add_plugins(audio::AudioPlugin);
app.add_plugins(ui::UiPlugin);
app.add_plugins(tweak::TweakPlugin);
app.add_plugins(intro::IntroPlugin);
app.add_plugins(tutorial::TutorialPlugin);
app.add_plugins(ai::AiPlugin);
app.add_plugins(audio::AudioPlugin);
app.run();
}
@ -143,6 +148,7 @@ pub(crate) fn _any_component_added_or_changed<C: Component>(
!q.is_empty()
}
#[cfg(not(target_os = "macos"))]
fn set_window_icon(
// we have to use `NonSend` here
windows: NonSend<WinitWindows>,
@ -186,10 +192,7 @@ where
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;
query.iter_mut().for_each(|mut window| {
@ -198,4 +201,4 @@ fn handle_window(
_ => WindowMode::BorderlessFullscreen,
}
})
}
}

@ -26,7 +26,10 @@ impl Plugin for MenuPlugin {
)
.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)));
}
}
@ -386,11 +389,10 @@ fn handle_button_press<S: States + Clone + Component>(
});
}
fn manage_ai_button(
curr: Res<State<ai::PlayState>>,
mut query: Query<(&mut ButtonAction<PlayState>, &Children)>,
mut texts: Query<&mut Text>,
mut query: Query<(&mut ButtonAction<PlayState>, &Children)>,
mut texts: Query<&mut Text>,
) {
query.iter_mut().for_each(|(mut ba, children)| {
ba.0 = match curr.get() {
@ -403,7 +405,7 @@ fn manage_ai_button(
t.sections.iter_mut().for_each(|s| {
s.value = match ba.0 {
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::{
animation::RepeatAnimation,

@ -20,18 +20,18 @@ impl Plugin for TutorialPlugin {
// tutorial must be running
.run_if(not(in_state(TutorialState::None)))
.run_if(
// A piece changes sides
any_component_changed::<game::Side>()
// When a piece is selected, we
.or_else(any_component_added::<game::Selected>())
// A piece is de-selected
.or_else(any_component_removed::<game::Selected>())
// TEMP: The user hits 'enter'
.or_else(
just_pressed(KeyCode::Enter)
.or_else(just_pressed(MouseButton::Left)),
),
),
// A piece changes sides
any_component_changed::<game::Side>()
// When a piece is selected, we
.or_else(any_component_added::<game::Selected>())
// A piece is de-selected
.or_else(any_component_removed::<game::Selected>())
// TEMP: The user hits 'enter'
.or_else(
just_pressed(KeyCode::Enter)
.or_else(just_pressed(MouseButton::Left)),
),
),
),
)
// Manage visible/hidden states
@ -41,8 +41,8 @@ impl Plugin for TutorialPlugin {
manage_state_entities::<TutorialState>(),
activate_tutorial_step,
)
.run_if(not(in_state(GameState::Loading)))
.run_if(state_changed::<TutorialState>),
.run_if(not(in_state(GameState::Loading)))
.run_if(state_changed::<TutorialState>),
)
.add_systems(
Update,
@ -52,8 +52,9 @@ impl Plugin for TutorialPlugin {
.run_if(in_state(MenuState::Off))
.run_if(not(resource_exists::<TutorialStarted>))
.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)
else {
// 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)| {
if *side == side_state.get().0 {
if !queen_seen && *piece == Piece::Queen {
@ -576,7 +578,8 @@ fn start_tutorial_on_play(
mut next_state: ResMut<NextState<TutorialState>>,
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!");
next_state.set(TutorialState::Intro);
commands.insert_resource(TutorialStarted);
@ -587,5 +590,8 @@ fn clear_tutorial_progress(
mut events: EventReader<StateTransitionEvent<TutorialState>>,
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(
tweaks_file: Res<tweak::GameTweaks>,
tweaks: Res<Assets<tweak::Tweaks>>,
mut commands: Commands,
mut commands: Commands,
) {
let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks");
let button_handle = tweak.get_handle::<Image>("buttons_image_resting").unwrap();

Loading…
Cancel
Save