Compare commits
3 commits
master
...
wasm-threa
Author | SHA1 | Date | |
---|---|---|---|
61cb0ce6b4 | |||
cc2877c9d8 | |||
d61fdb2382 |
32 changed files with 1235 additions and 2709 deletions
843
Cargo.lock
generated
843
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
23
Cargo.toml
23
Cargo.toml
|
@ -1,33 +1,30 @@
|
|||
[package]
|
||||
name = "lux-synthese"
|
||||
name = "bevyjam"
|
||||
version = "0.1.0"
|
||||
authors = ["tuxmain <tuxmain@zettascript.org>"]
|
||||
license = "AGPL-3.0-only"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.8.1", default-features = false, features = ["bevy_asset", "bevy_audio", "bevy_gilrs", "bevy_winit", "render", "png", "vorbis", "x11"] }
|
||||
bevy = "0.8.1"
|
||||
bevy_common_assets = { version = "0.3.0", features = ["json"] }
|
||||
bevy-inspector-egui = "0.12.1"
|
||||
bevy_rapier2d = "0.16.2"
|
||||
#crossbeam-channel = "0.5.6"
|
||||
crossbeam-channel = "0.5.6"
|
||||
rand = "0.8.5"
|
||||
rand_distr = "0.4.3"
|
||||
rapier2d = "0.14.0"
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
ticktock = "0.8.0"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
||||
#bevy-inspector-egui = "0.12.1"
|
||||
bevy_mod_picking = "0.9.0"
|
||||
# cpal = "0.14.0"
|
||||
# hexodsp = { git = "https://github.com/WeirdConstructor/HexoDSP", default-features = false }
|
||||
serde_json = "1.0.85"
|
||||
#ticktock = "0.8.0"
|
||||
hexodsp = { git = "https://git.txmn.tk/tuxmain/HexoDSP", default-features = false }
|
||||
cpal = "0.14.0"
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies]
|
||||
#cpal = { version = "0.14.0", features = ["wasm-bindgen"] }
|
||||
cpal = { version = "0.14.0", features = ["wasm-bindgen"] }
|
||||
wasm_thread = { git = "https://github.com/chemicstry/wasm_thread" }
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
wgpu = { git = "https://github.com/mockersf/wgpu/", branch = "unconditional-clear-workaround" }
|
||||
|
|
52
README.md
52
README.md
|
@ -1,14 +1,4 @@
|
|||
# Lux synthesĕ
|
||||
|
||||
Combine colored lights to synthesize white light!
|
||||
|
||||
[Play in browser](https://txmn.tk/projects/lux-synthese/)
|
||||
|
||||
Download: [Linux x86_64](https://txmn.tk/projects/lux-synthese/linux-x86_64.zip), [Windows x86_64](https://txmn.tk/projects/lux-synthese/windows-x86_64.zip)
|
||||
|
||||
In latin, _lux synthesĕ_ means "light by the means of synthesis".
|
||||
|
||||
This game was developped for the [Bevy Jam #2](https://itch.io/jam/bevy-jam-2).
|
||||
# Bevyjam
|
||||
|
||||
## Controls
|
||||
|
||||
|
@ -16,17 +6,18 @@ This game was developped for the [Bevy Jam #2](https://itch.io/jam/bevy-jam-2).
|
|||
* **Switch character**: Tab
|
||||
* **Level up**: Enter (when character is white)
|
||||
* **Reset**: R
|
||||
* **Fullscreen**: F11
|
||||
* **Exit**: Escape
|
||||
|
||||
## TODO
|
||||
|
||||
* name
|
||||
* more filters
|
||||
* more levels
|
||||
* despawn black characters
|
||||
* despawn character when too far
|
||||
* level design
|
||||
* (?) multiplayer
|
||||
* more audio
|
||||
* bug: when reset after win, character colors are wrong
|
||||
* redshift warning
|
||||
* "jumpable" component to avoid jumping on sensors
|
||||
* bug: in level2, move the blue character to win, then reset. The characters are lighter than expected.
|
||||
|
||||
## Build
|
||||
|
||||
|
@ -36,6 +27,8 @@ cargo build --release
|
|||
|
||||
### WASM
|
||||
|
||||
**Audio does not work in WASM!**
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo install wasm-bindgen-cli
|
||||
|
@ -48,33 +41,14 @@ python3 -m http.server
|
|||
|
||||
## Audio
|
||||
|
||||
This game used [HexoDSP](https://github.com/WeirdConstructor/HexoDSP) for audio synthesis.
|
||||
This game uses [HexoDSP](https://github.com/WeirdConstructor/HexoDSP) for audio synthesis.
|
||||
|
||||
The synthetizer matrix can be edited using [HexoSynth](https://github.com/WeirdConstructor/HexoSynth) visual editor.
|
||||
|
||||
However we could not get this working in WASM, hence we used simple playback audio instead for the jam. The concept of procedural audio is not abandonned but it will be for another time.
|
||||
|
||||
## Develop
|
||||
|
||||
Skip to level `N: u32` with the command `bevyjam <N>`.
|
||||
|
||||
Edit the level `N: u32` with the command `bevyjam <N> e`.
|
||||
|
||||
Editor is not available in the WASM build.
|
||||
|
||||
### Editor controls
|
||||
|
||||
* **Select**: left click to select, click in void to deselect, CTRL+click to select many, CTRL+A to select all
|
||||
* **Move selection**: arrows to move one step, Shift+arrows to move continuously
|
||||
* **Delete selection**: delete
|
||||
* **Add objects**: P=platform, C=character, A=absorbing filter, R=rotating filter, M=melty platform
|
||||
* **Move camera**: CTRL+arrows
|
||||
* **Save**: CTRL+S
|
||||
|
||||
## License
|
||||
|
||||
GNU AGPL v3, CopyLeft 2022 Pascal Engélibert, Nixon Cheng
|
||||
|
||||
_Lux synthesĕ_ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.
|
||||
_Lux synthesĕ_ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License along with _Lux synthesĕ_. If not, see https://www.gnu.org/licenses/.
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
|
||||
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
|
||||
|
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
BIN
assets/melty.png
BIN
assets/melty.png
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,15 +0,0 @@
|
|||
# WASM
|
||||
|
||||
sh build-wasm.sh || exit 1
|
||||
|
||||
mkdir -p target/itchio/wasm/target
|
||||
mkdir -p target/itchio/wasm/assets
|
||||
|
||||
cp -r assets/* target/itchio/wasm/assets/
|
||||
cp index.html target/itchio/wasm/
|
||||
cp target/lux-synthese.js target/itchio/wasm/target/
|
||||
cp target/lux-synthese_bg.wasm target/itchio/wasm/target/
|
||||
jq -c < assets/game.levels.json > target/itchio/wasm/assets/game.levels.json
|
||||
|
||||
cd target/itchio/wasm
|
||||
zip -r ../wasm.zip .
|
|
@ -1,3 +1,6 @@
|
|||
cargo build --release --target wasm32-unknown-unknown || exit 1
|
||||
echo "==> wasm-bindgen..."
|
||||
wasm-bindgen --out-name lux-synthese --out-dir target --target web target/wasm32-unknown-unknown/release/lux-synthese.wasm || exit 1
|
||||
cargo build --release --target wasm32-unknown-unknown
|
||||
# wasm-bindgen --out-name bevyjam --out-dir target --target web target/wasm32-unknown-unknown/release/bevyjam.wasm
|
||||
wasm-bindgen target/wasm32-unknown-unknown/release/bevyjam.wasm \
|
||||
--out-name bevyjam \
|
||||
--out-dir target \
|
||||
--target no-modules
|
||||
|
|
91
index.html
91
index.html
|
@ -2,91 +2,16 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Lux synthesĕ</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: UacariLegacy;
|
||||
font-weight: normal;
|
||||
src: url("assets/UacariLegacy-Regular.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: UacariLegacy;
|
||||
font-weight: bold;
|
||||
src: url("assets/UacariLegacy-Bold.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: UacariLegacy-Thin;
|
||||
src: url("assets/UacariLegacy-Thin.ttf");
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: UacariLegacy;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #222;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
color: white;
|
||||
}
|
||||
h1, h2 {
|
||||
font-family: UacariLegacy-Thin;
|
||||
font-weight: normal;
|
||||
}
|
||||
canvas {
|
||||
order: 1;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
margin: auto;
|
||||
}
|
||||
#readme {
|
||||
order: 2;
|
||||
width: 640px;
|
||||
max-width: 100vw;
|
||||
padding: 8px;
|
||||
margin: auto;
|
||||
}
|
||||
a, a:visited {
|
||||
color: #f80;
|
||||
}
|
||||
</style>
|
||||
<title>Bevyjam</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import init from './target/lux-synthese.js'
|
||||
init()
|
||||
<script src="./target/bevyjam.js"></script>
|
||||
<script type="text/javascript">
|
||||
wasm_bindgen("./target/bevyjam_bg.wasm").then((wasm) =>{
|
||||
wasm.run();
|
||||
});
|
||||
// import init from './target/bevyjam.js'
|
||||
// init()
|
||||
</script>
|
||||
<!-- workaround to be sure the browser will request permission to autoplay audio -->
|
||||
<audio autoplay src="assets/silence.ogg"></audio>
|
||||
<div id="readme">
|
||||
<h1>Lux synthesĕ</h1>
|
||||
<p>
|
||||
<strong>If audio does not work, please allow media autoplay in browser, then refresh this page.</strong>
|
||||
</p>
|
||||
<h2>Controls</h2>
|
||||
<ul>
|
||||
<li><strong>Move</strong>: Arrows</li>
|
||||
<li><strong>Switch</strong>: Tab</li>
|
||||
<li><strong>Level up</strong>: Enter</li>
|
||||
<li><strong>Reset</strong>: R</li>
|
||||
</ul>
|
||||
<h2>Download</h2>
|
||||
Download an executable release:
|
||||
<ul>
|
||||
<li><a href="https://txmn.tk/projects/lux-synthese/linux-x86_64.zip">Linux x86_64</a></li>
|
||||
<li><a href="https://txmn.tk/projects/lux-synthese/windows-x86_64.zip">Windows x86_64</a></li>
|
||||
</ul>
|
||||
<h2>Source</h2>
|
||||
<p>
|
||||
The source code of this free software is available in our <a href="https://git.txmn.tk/tuxmain/bevyjam/">Git repository</a>.
|
||||
</p>
|
||||
<p>
|
||||
GNU AGPL v3: CopyLeft 2022 Pascal Engélibert, Nixon Cheng<br/>
|
||||
Lux synthesĕ is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License.<br/>
|
||||
Lux synthesĕ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.<br/>
|
||||
You should have received a copy of the GNU Affero General Public License along with Lux synthesĕ. If not, see <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
BIN
res/cover.png
BIN
res/cover.png
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
BIN
res/cover.xcf
BIN
res/cover.xcf
Binary file not shown.
BIN
res/notes.flp
BIN
res/notes.flp
Binary file not shown.
|
@ -1,6 +1,5 @@
|
|||
// https://github.com/WeirdConstructor/HexoDSP/blob/master/examples/cpal_demo_node_api.rs
|
||||
|
||||
// use bevy::{prelude::*, asset::HandleId};
|
||||
use crate::game::AudioMsg;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
pub struct AudioSystemPlugin;
|
||||
|
||||
impl Plugin for AudioSystemPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<AudioAssets>()
|
||||
.add_startup_system(load_audio);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AudioAssets {
|
||||
pub notes: [Handle<AudioSource>; 3],
|
||||
pub reverb_notes: [Handle<AudioSource>; 3],
|
||||
pub warp_notes: [Handle<AudioSource>; 3],
|
||||
}
|
||||
|
||||
pub fn play_audio(
|
||||
audio_set: &[Handle<AudioSource>; 3],
|
||||
audio: &Res<Audio>,
|
||||
color: Vec4,
|
||||
volume: f32,
|
||||
) {
|
||||
for i in 0..3 {
|
||||
audio.play_with_settings(
|
||||
audio_set[i].clone(),
|
||||
PlaybackSettings::ONCE.with_volume(color[i] * volume),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_audio(mut audio_assets: ResMut<AudioAssets>, asset_server: Res<AssetServer>) {
|
||||
audio_assets.notes[0] = asset_server.load::<AudioSource, &str>("sound/notes/a5.ogg");
|
||||
audio_assets.notes[1] = asset_server.load::<AudioSource, &str>("sound/notes/cs6.ogg");
|
||||
audio_assets.notes[2] = asset_server.load::<AudioSource, &str>("sound/notes/e6.ogg");
|
||||
|
||||
audio_assets.reverb_notes[0] =
|
||||
asset_server.load::<AudioSource, &str>("sound/reverb_notes/a5.ogg");
|
||||
audio_assets.reverb_notes[1] =
|
||||
asset_server.load::<AudioSource, &str>("sound/reverb_notes/cs6.ogg");
|
||||
audio_assets.reverb_notes[2] =
|
||||
asset_server.load::<AudioSource, &str>("sound/reverb_notes/e6.ogg");
|
||||
|
||||
audio_assets.warp_notes[0] = asset_server.load::<AudioSource, &str>("sound/warp_notes/a5.ogg");
|
||||
audio_assets.warp_notes[1] = asset_server.load::<AudioSource, &str>("sound/warp_notes/cs6.ogg");
|
||||
audio_assets.warp_notes[2] = asset_server.load::<AudioSource, &str>("sound/warp_notes/e6.ogg");
|
||||
}
|
813
src/editor.rs
813
src/editor.rs
|
@ -1,813 +0,0 @@
|
|||
#![allow(clippy::type_complexity)]
|
||||
use crate::{levels::stored::*, AppState};
|
||||
|
||||
use bevy::{
|
||||
input::{keyboard::KeyCode, Input},
|
||||
prelude::{
|
||||
shape::{Circle, Quad},
|
||||
*,
|
||||
},
|
||||
sprite::Mesh2dHandle,
|
||||
};
|
||||
use bevy_mod_picking::*;
|
||||
|
||||
pub struct EditorPlugin;
|
||||
|
||||
impl Plugin for EditorPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<DragEndEvent>()
|
||||
.add_plugins(DefaultPickingPlugins)
|
||||
.add_system_set(SystemSet::on_enter(AppState::Editor).with_system(setup))
|
||||
.add_system_set(
|
||||
SystemSet::on_update(AppState::Editor)
|
||||
.with_system(move_system)
|
||||
.with_system(input_control_system)
|
||||
.with_system(follow_ends_system)
|
||||
.with_system(remove_system)
|
||||
.with_system(spawn_system),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Events
|
||||
|
||||
struct DragEndEvent(Entity);
|
||||
|
||||
// Resources
|
||||
|
||||
struct CharacterList(Vec<Entity>);
|
||||
|
||||
// Components
|
||||
|
||||
#[derive(Component)]
|
||||
struct Platform;
|
||||
|
||||
#[derive(Component)]
|
||||
struct Ends(Entity, Entity);
|
||||
|
||||
#[derive(Component)]
|
||||
struct Draggable;
|
||||
|
||||
#[derive(Component)]
|
||||
struct End(Entity);
|
||||
|
||||
#[derive(Component)]
|
||||
struct CharacterColor(Color);
|
||||
|
||||
#[derive(Component)]
|
||||
struct Size(Vec2);
|
||||
|
||||
#[derive(Component)]
|
||||
struct RotatingFilterAngle(f32);
|
||||
|
||||
#[derive(Component)]
|
||||
struct AbsorbingFilterColor(Color);
|
||||
|
||||
#[derive(Component)]
|
||||
struct MeltyPlatformColor(Color);
|
||||
|
||||
#[derive(Component)]
|
||||
struct Removable;
|
||||
|
||||
// Bundles
|
||||
|
||||
#[derive(Bundle)]
|
||||
struct PlatformBundle {
|
||||
#[bundle]
|
||||
mesh: ColorMesh2dBundle,
|
||||
size: Size,
|
||||
platform: Platform,
|
||||
#[bundle]
|
||||
pickable: PickableBundle,
|
||||
removable: Removable,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
struct EndBundle {
|
||||
#[bundle]
|
||||
mesh: ColorMesh2dBundle,
|
||||
#[bundle]
|
||||
pickable: PickableBundle,
|
||||
draggable: Draggable,
|
||||
end: End,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
struct CharacterBundle {
|
||||
#[bundle]
|
||||
mesh: ColorMesh2dBundle,
|
||||
color: CharacterColor,
|
||||
#[bundle]
|
||||
pickable: PickableBundle,
|
||||
draggable: Draggable,
|
||||
removable: Removable,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
struct AbsorbingFilterBundle {
|
||||
#[bundle]
|
||||
mesh: ColorMesh2dBundle,
|
||||
size: Size,
|
||||
color: AbsorbingFilterColor,
|
||||
#[bundle]
|
||||
pickable: PickableBundle,
|
||||
removable: Removable,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
struct RotatingFilterBundle {
|
||||
#[bundle]
|
||||
mesh: ColorMesh2dBundle,
|
||||
angle: RotatingFilterAngle,
|
||||
#[bundle]
|
||||
pickable: PickableBundle,
|
||||
draggable: Draggable,
|
||||
removable: Removable,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
struct MeltyPlatformBundle {
|
||||
#[bundle]
|
||||
mesh: ColorMesh2dBundle,
|
||||
color: MeltyPlatformColor,
|
||||
#[bundle]
|
||||
pickable: PickableBundle,
|
||||
draggable: Draggable,
|
||||
removable: Removable,
|
||||
}
|
||||
|
||||
// Functions
|
||||
|
||||
fn spawn_platform(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
|
||||
transform: Transform,
|
||||
size: Vec2,
|
||||
) {
|
||||
let platform = commands
|
||||
.spawn_bundle(PlatformBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes.add(Mesh::from(Quad { size, flip: false })).into(),
|
||||
material: materials.add(ColorMaterial::from(Color::GRAY)),
|
||||
transform,
|
||||
..default()
|
||||
},
|
||||
size: Size(size),
|
||||
platform: Platform,
|
||||
pickable: PickableBundle::default(),
|
||||
removable: Removable,
|
||||
})
|
||||
.id();
|
||||
let ends = Ends(
|
||||
commands
|
||||
.spawn_bundle(EndBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Circle {
|
||||
radius: 8.,
|
||||
vertices: 12,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
|
||||
transform: Transform::from_xyz(
|
||||
transform.translation.x - size.x / 2.,
|
||||
transform.translation.y - size.y / 2.,
|
||||
0.5,
|
||||
),
|
||||
..default()
|
||||
},
|
||||
pickable: PickableBundle::default(),
|
||||
draggable: Draggable,
|
||||
end: End(platform),
|
||||
})
|
||||
.id(),
|
||||
commands
|
||||
.spawn_bundle(EndBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Circle {
|
||||
radius: 8.,
|
||||
vertices: 12,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
|
||||
transform: Transform::from_xyz(
|
||||
transform.translation.x + size.x / 2.,
|
||||
transform.translation.y + size.y / 2.,
|
||||
0.5,
|
||||
),
|
||||
..default()
|
||||
},
|
||||
pickable: PickableBundle::default(),
|
||||
draggable: Draggable,
|
||||
end: End(platform),
|
||||
})
|
||||
.id(),
|
||||
);
|
||||
commands.entity(platform).insert(ends);
|
||||
}
|
||||
|
||||
fn spawn_character(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
|
||||
transform: Transform,
|
||||
color: Color,
|
||||
index: usize,
|
||||
) -> Entity {
|
||||
let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
|
||||
commands
|
||||
.spawn_bundle(CharacterBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Quad {
|
||||
size: Vec2 { x: 64., y: 64. },
|
||||
flip: false,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(color)),
|
||||
transform,
|
||||
..default()
|
||||
},
|
||||
color: CharacterColor(color),
|
||||
pickable: PickableBundle::default(),
|
||||
draggable: Draggable,
|
||||
removable: Removable,
|
||||
})
|
||||
.with_children(|c| {
|
||||
c.spawn_bundle(Text2dBundle {
|
||||
text: Text::from_section(
|
||||
&index.to_string(),
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: 32.,
|
||||
color: Color::WHITE,
|
||||
},
|
||||
)
|
||||
.with_alignment(TextAlignment::CENTER),
|
||||
transform: Transform::from_xyz(0., 0., 1.),
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.id()
|
||||
}
|
||||
|
||||
fn spawn_absorbing_filter(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
|
||||
transform: Transform,
|
||||
size: Vec2,
|
||||
color: Color,
|
||||
) {
|
||||
let absorbing_filter = commands
|
||||
.spawn_bundle(AbsorbingFilterBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes.add(Mesh::from(Quad { size, flip: false })).into(),
|
||||
material: materials.add(ColorMaterial::from(color)),
|
||||
transform,
|
||||
..default()
|
||||
},
|
||||
size: Size(size),
|
||||
color: AbsorbingFilterColor(color),
|
||||
pickable: PickableBundle::default(),
|
||||
removable: Removable,
|
||||
})
|
||||
.id();
|
||||
let ends = Ends(
|
||||
commands
|
||||
.spawn_bundle(EndBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Circle {
|
||||
radius: 8.,
|
||||
vertices: 12,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
|
||||
transform: Transform::from_xyz(
|
||||
transform.translation.x - size.x / 2.,
|
||||
transform.translation.y - size.y / 2.,
|
||||
0.5,
|
||||
),
|
||||
..default()
|
||||
},
|
||||
pickable: PickableBundle::default(),
|
||||
draggable: Draggable,
|
||||
end: End(absorbing_filter),
|
||||
})
|
||||
.id(),
|
||||
commands
|
||||
.spawn_bundle(EndBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Circle {
|
||||
radius: 8.,
|
||||
vertices: 12,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(Color::rgba(1., 1., 0., 0.7))),
|
||||
transform: Transform::from_xyz(
|
||||
transform.translation.x + size.x / 2.,
|
||||
transform.translation.y + size.y / 2.,
|
||||
0.5,
|
||||
),
|
||||
..default()
|
||||
},
|
||||
pickable: PickableBundle::default(),
|
||||
draggable: Draggable,
|
||||
end: End(absorbing_filter),
|
||||
})
|
||||
.id(),
|
||||
);
|
||||
commands.entity(absorbing_filter).insert(ends);
|
||||
}
|
||||
|
||||
fn spawn_rotating_filter(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
|
||||
transform: Transform,
|
||||
angle: f32,
|
||||
) -> Entity {
|
||||
let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
|
||||
commands
|
||||
.spawn_bundle(RotatingFilterBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Circle {
|
||||
radius: 32.,
|
||||
vertices: 36,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(Color::WHITE)),
|
||||
transform,
|
||||
..default()
|
||||
},
|
||||
angle: RotatingFilterAngle(angle),
|
||||
pickable: PickableBundle::default(),
|
||||
draggable: Draggable,
|
||||
removable: Removable,
|
||||
})
|
||||
.with_children(|c| {
|
||||
c.spawn_bundle(Text2dBundle {
|
||||
text: Text::from_section(
|
||||
&angle.to_string(),
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: 32.,
|
||||
color: Color::RED,
|
||||
},
|
||||
)
|
||||
.with_alignment(TextAlignment::CENTER),
|
||||
transform: Transform::from_xyz(0., 0., 1.),
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.id()
|
||||
}
|
||||
|
||||
fn spawn_melty_platform(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
|
||||
transform: Transform,
|
||||
color: Color,
|
||||
) -> Entity {
|
||||
let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
|
||||
commands
|
||||
.spawn_bundle(MeltyPlatformBundle {
|
||||
mesh: ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Quad {
|
||||
size: Vec2 { x: 96., y: 16. },
|
||||
flip: false,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(color)),
|
||||
transform,
|
||||
..default()
|
||||
},
|
||||
color: MeltyPlatformColor(color),
|
||||
pickable: PickableBundle::default(),
|
||||
draggable: Draggable,
|
||||
removable: Removable,
|
||||
})
|
||||
.with_children(|c| {
|
||||
c.spawn_bundle(Text2dBundle {
|
||||
text: Text::from_section(
|
||||
"MELTY",
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: 16.,
|
||||
color: Color::BLACK,
|
||||
},
|
||||
)
|
||||
.with_alignment(TextAlignment::CENTER),
|
||||
transform: Transform::from_xyz(0., 0., 1.),
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.id()
|
||||
}
|
||||
|
||||
fn spawn_stored_level(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
|
||||
stored_level: &StoredLevel,
|
||||
) {
|
||||
for platform in stored_level.platforms.iter() {
|
||||
spawn_platform(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
Transform::from_xyz(platform.pos.x, platform.pos.y, 0.),
|
||||
platform.size,
|
||||
);
|
||||
}
|
||||
|
||||
let mut character_list = Vec::new();
|
||||
for (i, character) in stored_level.characters.iter().enumerate() {
|
||||
character_list.push(spawn_character(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
asset_server,
|
||||
Transform::from_xyz(character.pos.x, character.pos.y, 0.),
|
||||
character.color.into(),
|
||||
i,
|
||||
));
|
||||
}
|
||||
commands.insert_resource(CharacterList(character_list));
|
||||
|
||||
for absorbing_filter in stored_level.absorbing_filters.iter() {
|
||||
spawn_absorbing_filter(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
Transform::from_xyz(absorbing_filter.pos.x, absorbing_filter.pos.y, 0.),
|
||||
absorbing_filter.size,
|
||||
absorbing_filter.color.into(),
|
||||
);
|
||||
}
|
||||
|
||||
for rotating_filter in stored_level.rotating_filters.iter() {
|
||||
spawn_rotating_filter(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
asset_server,
|
||||
Transform::from_xyz(rotating_filter.pos.x, rotating_filter.pos.y, 0.),
|
||||
rotating_filter.angle,
|
||||
);
|
||||
}
|
||||
|
||||
for melty_platform in stored_level.melty_platforms.iter() {
|
||||
spawn_melty_platform(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
asset_server,
|
||||
Transform::from_xyz(melty_platform.pos.x, melty_platform.pos.y, 0.),
|
||||
melty_platform.color.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn save_level(
|
||||
level_id: &Res<crate::game::FirstLevel>,
|
||||
stored_levels_assets: &mut ResMut<Assets<StoredLevels>>,
|
||||
stored_levels_handle: &Res<Handle<StoredLevels>>,
|
||||
character_list: &Res<CharacterList>,
|
||||
character_query: &Query<(&Transform, &CharacterColor), Without<Platform>>,
|
||||
platform_query: &Query<(&Transform, &Size), With<Platform>>,
|
||||
absorbing_filter_query: &Query<(&Transform, &Size, &AbsorbingFilterColor), Without<Platform>>,
|
||||
rotating_filter_query: &Query<
|
||||
(&Transform, &RotatingFilterAngle),
|
||||
(Without<Platform>, Without<CharacterColor>),
|
||||
>,
|
||||
melty_platform_query: &Query<
|
||||
(&Transform, &MeltyPlatformColor),
|
||||
(Without<Platform>, Without<CharacterColor>),
|
||||
>,
|
||||
) {
|
||||
let stored_levels = stored_levels_assets.get_mut(stored_levels_handle).unwrap();
|
||||
if let Some(stored_level) = stored_levels.levels.get_mut(level_id.0 .0 as usize) {
|
||||
stored_level.platforms.clear();
|
||||
for (transform, size) in platform_query.iter() {
|
||||
stored_level.platforms.push(StoredPlatform {
|
||||
pos: Vec2 {
|
||||
x: transform.translation.x,
|
||||
y: transform.translation.y,
|
||||
},
|
||||
size: size.0,
|
||||
})
|
||||
}
|
||||
|
||||
stored_level.characters.clear();
|
||||
for entity in character_list.0.iter() {
|
||||
if let Ok((transform, color)) = character_query.get(*entity) {
|
||||
stored_level.characters.push(StoredCharacter {
|
||||
pos: Vec2 {
|
||||
x: transform.translation.x,
|
||||
y: transform.translation.y,
|
||||
},
|
||||
color: color.0.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
stored_level.absorbing_filters.clear();
|
||||
for (transform, size, color) in absorbing_filter_query.iter() {
|
||||
stored_level.absorbing_filters.push(StoredAbsorbingFilter {
|
||||
pos: Vec2 {
|
||||
x: transform.translation.x,
|
||||
y: transform.translation.y,
|
||||
},
|
||||
size: size.0,
|
||||
color: color.0.into(),
|
||||
})
|
||||
}
|
||||
|
||||
stored_level.rotating_filters.clear();
|
||||
for (transform, angle) in rotating_filter_query.iter() {
|
||||
stored_level.rotating_filters.push(StoredRotatingFilter {
|
||||
pos: Vec2 {
|
||||
x: transform.translation.x,
|
||||
y: transform.translation.y,
|
||||
},
|
||||
angle: angle.0,
|
||||
})
|
||||
}
|
||||
|
||||
stored_level.melty_platforms.clear();
|
||||
for (transform, color) in melty_platform_query.iter() {
|
||||
stored_level.melty_platforms.push(StoredMeltyPlatform {
|
||||
pos: Vec2 {
|
||||
x: transform.translation.x,
|
||||
y: transform.translation.y,
|
||||
},
|
||||
color: color.0.into(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
match std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open("assets/game.levels.json")
|
||||
{
|
||||
Ok(mut file) => {
|
||||
serde_json::to_writer_pretty(&mut file, stored_levels).unwrap();
|
||||
println!("Saved!");
|
||||
}
|
||||
Err(e) => eprintln!("Error writing levels file: {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Systems
|
||||
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
camera_query: Query<Entity, With<Camera>>,
|
||||
level_id: Res<crate::game::FirstLevel>,
|
||||
stored_levels_assets: Res<Assets<StoredLevels>>,
|
||||
stored_levels_handle: Res<Handle<StoredLevels>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
commands
|
||||
.entity(camera_query.single())
|
||||
.insert_bundle(PickingCameraBundle::default());
|
||||
|
||||
if let Some(stored_level) = stored_levels_assets
|
||||
.get(&stored_levels_handle)
|
||||
.unwrap()
|
||||
.levels
|
||||
.get(level_id.0 .0 as usize)
|
||||
{
|
||||
spawn_stored_level(
|
||||
&mut commands,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
&asset_server,
|
||||
stored_level,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn input_control_system(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
level_id: Res<crate::game::FirstLevel>,
|
||||
mut stored_levels_assets: ResMut<Assets<StoredLevels>>,
|
||||
stored_levels_handle: Res<Handle<StoredLevels>>,
|
||||
character_list: Res<CharacterList>,
|
||||
character_query: Query<(&Transform, &CharacterColor), Without<Platform>>,
|
||||
platform_query: Query<(&Transform, &Size), With<Platform>>,
|
||||
absorbing_filter_query: Query<(&Transform, &Size, &AbsorbingFilterColor), Without<Platform>>,
|
||||
rotating_filter_query: Query<
|
||||
(&Transform, &RotatingFilterAngle),
|
||||
(Without<Platform>, Without<CharacterColor>),
|
||||
>,
|
||||
melty_platform_query: Query<
|
||||
(&Transform, &MeltyPlatformColor),
|
||||
(Without<Platform>, Without<CharacterColor>),
|
||||
>,
|
||||
) {
|
||||
if keyboard_input.just_pressed(KeyCode::S)
|
||||
&& (keyboard_input.pressed(KeyCode::LControl) || keyboard_input.pressed(KeyCode::RControl))
|
||||
{
|
||||
save_level(
|
||||
&level_id,
|
||||
&mut stored_levels_assets,
|
||||
&stored_levels_handle,
|
||||
&character_list,
|
||||
&character_query,
|
||||
&platform_query,
|
||||
&absorbing_filter_query,
|
||||
&rotating_filter_query,
|
||||
&melty_platform_query,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn move_system(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut camera_query: Query<&mut Transform, (With<Camera>, Without<Draggable>)>,
|
||||
mut drag_query: Query<(&mut Transform, &Selection, Option<&End>), With<Draggable>>,
|
||||
mut drag_end_event: EventWriter<DragEndEvent>,
|
||||
) {
|
||||
if keyboard_input.pressed(KeyCode::LControl) || keyboard_input.pressed(KeyCode::RControl) {
|
||||
let mut transform = camera_query.single_mut();
|
||||
let drag = Vec3 {
|
||||
x: (keyboard_input.pressed(KeyCode::Right) as i8
|
||||
- keyboard_input.pressed(KeyCode::Left) as i8) as _,
|
||||
y: (keyboard_input.pressed(KeyCode::Up) as i8
|
||||
- keyboard_input.pressed(KeyCode::Down) as i8) as _,
|
||||
z: 0.,
|
||||
} * 8.;
|
||||
transform.translation += drag;
|
||||
return;
|
||||
}
|
||||
|
||||
let drag = if keyboard_input.pressed(KeyCode::LShift) || keyboard_input.pressed(KeyCode::RShift)
|
||||
{
|
||||
Vec3 {
|
||||
x: (keyboard_input.pressed(KeyCode::Right) as i8
|
||||
- keyboard_input.pressed(KeyCode::Left) as i8) as _,
|
||||
y: (keyboard_input.pressed(KeyCode::Up) as i8
|
||||
- keyboard_input.pressed(KeyCode::Down) as i8) as _,
|
||||
z: 0.,
|
||||
}
|
||||
} else {
|
||||
Vec3 {
|
||||
x: (keyboard_input.just_pressed(KeyCode::Right) as i8
|
||||
- keyboard_input.just_pressed(KeyCode::Left) as i8) as _,
|
||||
y: (keyboard_input.just_pressed(KeyCode::Up) as i8
|
||||
- keyboard_input.just_pressed(KeyCode::Down) as i8) as _,
|
||||
z: 0.,
|
||||
}
|
||||
} * 8.;
|
||||
if drag != Vec3::ZERO {
|
||||
for (mut transform, selection, end) in drag_query.iter_mut() {
|
||||
if selection.selected() {
|
||||
transform.translation += drag;
|
||||
if let Some(End(entity)) = end {
|
||||
drag_end_event.send(DragEndEvent(*entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_system(
|
||||
mut commands: Commands,
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut removable_query: Query<(Entity, &Selection, Option<&Ends>), With<Removable>>,
|
||||
) {
|
||||
if keyboard_input.just_released(KeyCode::Delete) {
|
||||
for (entity, selection, ends) in removable_query.iter_mut() {
|
||||
if selection.selected() {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
if let Some(ends) = ends {
|
||||
commands.entity(ends.0).despawn_recursive();
|
||||
commands.entity(ends.1).despawn_recursive();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_system(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
camera_query: Query<&Transform, With<Camera>>,
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut character_list: ResMut<CharacterList>,
|
||||
) {
|
||||
if keyboard_input.pressed(KeyCode::LControl)
|
||||
|| keyboard_input.pressed(KeyCode::RControl)
|
||||
|| keyboard_input.pressed(KeyCode::LAlt)
|
||||
|| keyboard_input.pressed(KeyCode::RAlt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let camera_pos = camera_query.single().translation;
|
||||
|
||||
if keyboard_input.just_released(KeyCode::P) {
|
||||
spawn_platform(
|
||||
&mut commands,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
Transform::from_xyz(camera_pos.x, camera_pos.y, 0.),
|
||||
Vec2 { x: 96., y: 16. },
|
||||
);
|
||||
}
|
||||
if keyboard_input.just_released(KeyCode::C) {
|
||||
let index = character_list.0.len();
|
||||
character_list.0.push(spawn_character(
|
||||
&mut commands,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
&asset_server,
|
||||
Transform::from_xyz(camera_pos.x, camera_pos.y, 0.),
|
||||
Color::RED,
|
||||
index,
|
||||
));
|
||||
}
|
||||
if keyboard_input.just_released(KeyCode::A) {
|
||||
spawn_absorbing_filter(
|
||||
&mut commands,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
Transform::from_xyz(camera_pos.x, camera_pos.y, 0.),
|
||||
Vec2 { x: 16., y: 96. },
|
||||
Color::RED,
|
||||
);
|
||||
}
|
||||
if keyboard_input.just_released(KeyCode::R) {
|
||||
spawn_rotating_filter(
|
||||
&mut commands,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
&asset_server,
|
||||
Transform::from_xyz(camera_pos.x, camera_pos.y, 0.),
|
||||
90.,
|
||||
);
|
||||
}
|
||||
if keyboard_input.just_released(KeyCode::M) {
|
||||
spawn_melty_platform(
|
||||
&mut commands,
|
||||
&mut meshes,
|
||||
&mut materials,
|
||||
&asset_server,
|
||||
Transform::from_xyz(camera_pos.x, camera_pos.y, 0.),
|
||||
Color::RED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn follow_ends_system(
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut follower_query: Query<(&mut Transform, &mut Mesh2dHandle, &mut Size, &Ends)>,
|
||||
end_query: Query<&Transform, Without<Ends>>,
|
||||
mut drag_end_event: EventReader<DragEndEvent>,
|
||||
) {
|
||||
for DragEndEvent(entity) in drag_end_event.iter() {
|
||||
if let Ok((mut transform, mut mesh, mut size, Ends(end1, end2))) =
|
||||
follower_query.get_mut(*entity)
|
||||
{
|
||||
if let (Ok(end1), Ok(end2)) = (end_query.get(*end1), end_query.get(*end2)) {
|
||||
transform.translation.x = (end1.translation.x + end2.translation.x) / 2.;
|
||||
transform.translation.y = (end1.translation.y + end2.translation.y) / 2.;
|
||||
size.0 = Vec2 {
|
||||
x: (end2.translation.x - end1.translation.x).abs(),
|
||||
y: (end2.translation.y - end1.translation.y).abs(),
|
||||
};
|
||||
*mesh = meshes
|
||||
.add(Mesh::from(Quad {
|
||||
size: size.0,
|
||||
flip: false,
|
||||
}))
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,10 +23,6 @@ impl PassThroughFilter {
|
|||
PassThroughFilter::Rotating(filter_angle) => {
|
||||
let mut hsla = color.as_hsla_f32();
|
||||
hsla[0] = (hsla[0] + filter_angle) % 360.;
|
||||
// floating rem is not modulo!
|
||||
if hsla[0] < 0. {
|
||||
hsla[0] += 360.;
|
||||
}
|
||||
Color::hsla(hsla[0], hsla[1], hsla[2], hsla[3])
|
||||
}
|
||||
}
|
||||
|
|
470
src/game.rs
470
src/game.rs
|
@ -2,8 +2,8 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
pub use crate::filters::*;
|
||||
use crate::levels;
|
||||
use crate::{audio_system, AppState};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
use bevy::{
|
||||
ecs::system::EntityCommands,
|
||||
|
@ -13,7 +13,13 @@ use bevy::{
|
|||
};
|
||||
use bevy_rapier2d::prelude::*;
|
||||
use rapier2d::geometry::CollisionEventFlags;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub enum AudioMsg {
|
||||
Color([f32; 3]),
|
||||
Fusion,
|
||||
Jump,
|
||||
Switch,
|
||||
}
|
||||
|
||||
pub struct FirstLevel(pub LevelId);
|
||||
|
||||
|
@ -25,11 +31,8 @@ pub struct GamePlugin;
|
|||
impl Plugin for GamePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<LevelStartupEvent>()
|
||||
.add_event::<ChangeCharacterEvent>()
|
||||
.init_resource::<CharacterMeshes>()
|
||||
.insert_resource(CurrentLevel(None))
|
||||
.init_resource::<CharacterList>()
|
||||
.init_resource::<levels::ZoomTimer>()
|
||||
.add_system_set(SystemSet::on_enter(AppState::Game).with_system(setup))
|
||||
.add_system_set(SystemSet::on_enter(AppState::Win).with_system(win_setup))
|
||||
.add_system_set(
|
||||
|
@ -41,25 +44,17 @@ impl Plugin for GamePlugin {
|
|||
.with_system(change_character_system)
|
||||
.with_system(player_movement_system)
|
||||
.with_system(level_keyboard_system)
|
||||
.with_system(camera_system)
|
||||
.with_system(character_particle_effect_system)
|
||||
.with_system(kill_character_system),
|
||||
.with_system(move_camera)
|
||||
.with_system(character_particle_effect_system),
|
||||
)
|
||||
.add_system_set(
|
||||
SystemSet::on_update(AppState::Win)
|
||||
.with_system(player_movement_system)
|
||||
.with_system(level_keyboard_system)
|
||||
.with_system(camera_system)
|
||||
.with_system(character_particle_effect_system)
|
||||
.with_system(move_win_text_system),
|
||||
.with_system(move_camera)
|
||||
.with_system(character_particle_effect_system),
|
||||
)
|
||||
.add_system_to_stage(CoreStage::PostUpdate, char_char_collision_event_system)
|
||||
.add_system_to_stage(CoreStage::PostUpdate, char_platform_collision_event_system)
|
||||
// collision event system might remove items, therefore, we should detect platforms first before removing them
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
collision_event_system.after(char_platform_collision_event_system),
|
||||
);
|
||||
.add_system_to_stage(CoreStage::PostUpdate, collision_event_system);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,15 +62,10 @@ impl Plugin for GamePlugin {
|
|||
|
||||
pub struct LevelStartupEvent;
|
||||
|
||||
pub struct ChangeCharacterEvent;
|
||||
|
||||
// Resources
|
||||
|
||||
pub struct CurrentLevel(pub Option<LevelId>);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CharacterList(pub BTreeSet<Entity>);
|
||||
|
||||
pub struct CharacterMeshes {
|
||||
square: Mesh2dHandle,
|
||||
}
|
||||
|
@ -106,34 +96,7 @@ pub struct CharacterColor(pub Color);
|
|||
pub struct Player;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Platform;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct PlatformCount(usize);
|
||||
|
||||
impl PlatformCount {
|
||||
fn increment(&mut self) {
|
||||
self.0 += 1;
|
||||
}
|
||||
|
||||
fn decrement(&mut self) {
|
||||
self.0 = self.0.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.0 = 0;
|
||||
}
|
||||
|
||||
fn is_landed(&self) -> bool {
|
||||
self.0 != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Melty(pub Color);
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct WinText;
|
||||
pub struct CollisionCount(usize);
|
||||
|
||||
// Systems
|
||||
|
||||
|
@ -142,20 +105,19 @@ fn setup(
|
|||
mut current_level: ResMut<CurrentLevel>,
|
||||
mut level_startup_event: EventWriter<LevelStartupEvent>,
|
||||
mut camera_query: Query<&mut Transform, With<Camera>>,
|
||||
mut zoom_timer: ResMut<levels::ZoomTimer>,
|
||||
) {
|
||||
if current_level.0.is_none() {
|
||||
current_level.0 = Some(first_level.0);
|
||||
}
|
||||
|
||||
crate::levels::setup_level(&mut level_startup_event, &mut camera_query, &mut zoom_timer);
|
||||
crate::levels::setup_level(&mut level_startup_event, &mut camera_query);
|
||||
}
|
||||
|
||||
pub fn spawn_characters<I: IntoIterator<Item = (Transform, Color)>>(
|
||||
commands: &mut Commands,
|
||||
character_meshes: &Res<CharacterMeshes>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
character_list: &mut ResMut<CharacterList>,
|
||||
audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
|
||||
|
||||
characters: I,
|
||||
) {
|
||||
|
@ -164,7 +126,7 @@ pub fn spawn_characters<I: IntoIterator<Item = (Transform, Color)>>(
|
|||
commands,
|
||||
character_meshes,
|
||||
materials,
|
||||
character_list,
|
||||
audio,
|
||||
transform,
|
||||
color,
|
||||
i == 0,
|
||||
|
@ -176,7 +138,7 @@ pub fn spawn_character(
|
|||
commands: &mut Commands,
|
||||
character_meshes: &Res<CharacterMeshes>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
character_list: &mut ResMut<CharacterList>,
|
||||
audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
|
||||
mut transform: Transform,
|
||||
color: Color,
|
||||
is_player: bool,
|
||||
|
@ -213,13 +175,14 @@ pub fn spawn_character(
|
|||
.insert(Sensor)
|
||||
.insert(Collider::cuboid(30., 0.5))
|
||||
.insert(ActiveEvents::COLLISION_EVENTS)
|
||||
.insert(PlatformCount(0));
|
||||
.insert(CollisionCount(0));
|
||||
});
|
||||
|
||||
character_list.0.insert(entity_commands.id());
|
||||
|
||||
if is_player {
|
||||
entity_commands.insert(Player);
|
||||
audio
|
||||
.send(AudioMsg::Color([color.r(), color.g(), color.b()]))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,143 +214,12 @@ pub fn spawn_platform(
|
|||
..default()
|
||||
})
|
||||
.insert(Collider::cuboid(size.x / 2., size.y / 2.))
|
||||
.insert(Level)
|
||||
.insert(Platform);
|
||||
}
|
||||
|
||||
pub fn spawn_melty_platform(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
|
||||
transform: Transform,
|
||||
color: Color,
|
||||
) {
|
||||
commands
|
||||
.spawn_bundle(ColorMesh2dBundle {
|
||||
mesh: meshes
|
||||
.add(Mesh::from(Quad {
|
||||
size: Vec2 { x: 96., y: 16. },
|
||||
flip: false,
|
||||
}))
|
||||
.into(),
|
||||
material: materials.add(ColorMaterial::from(color)),
|
||||
transform,
|
||||
..default()
|
||||
})
|
||||
.insert(Collider::cuboid(48., 8.))
|
||||
.insert(Melty(color))
|
||||
.insert(Level)
|
||||
.insert(Platform)
|
||||
.with_children(|c| {
|
||||
c.spawn_bundle(SpriteBundle {
|
||||
texture: asset_server.get_handle("melty.png"),
|
||||
transform: Transform::from_xyz(0., 0., 0.5),
|
||||
..default()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn char_char_collision_event_system(
|
||||
mut commands: Commands,
|
||||
|
||||
mut collision_events: EventReader<CollisionEvent>,
|
||||
character_query: Query<(&CharacterColor, &Transform, Option<&Player>)>,
|
||||
|
||||
mut character_list: ResMut<CharacterList>,
|
||||
mut app_state: ResMut<State<AppState>>,
|
||||
character_meshes: Res<CharacterMeshes>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
audio_assets: Res<audio_system::AudioAssets>,
|
||||
audio: Res<Audio>,
|
||||
) {
|
||||
for collision_event in collision_events.iter() {
|
||||
if let CollisionEvent::Started(e1, e2, _flags) = collision_event {
|
||||
if let (
|
||||
Ok((c1_color, c1_transform, c1_player)),
|
||||
Ok((c2_color, c2_transform, c2_player)),
|
||||
) = (character_query.get(*e1), character_query.get(*e2))
|
||||
{
|
||||
character_list.0.remove(e1);
|
||||
character_list.0.remove(e2);
|
||||
commands.entity(*e1).despawn_recursive();
|
||||
commands.entity(*e2).despawn_recursive();
|
||||
|
||||
let new_color =
|
||||
(Vec4::from(c1_color.0) + Vec4::from(c2_color.0)).clamp(Vec4::ZERO, Vec4::ONE);
|
||||
|
||||
// If color approximately white
|
||||
if app_state.current() == &AppState::Game && new_color.min_element() >= 0.9 {
|
||||
app_state.replace(AppState::Win).ok();
|
||||
}
|
||||
|
||||
// position character based on current player location
|
||||
spawn_character(
|
||||
&mut commands,
|
||||
&character_meshes,
|
||||
&mut materials,
|
||||
&mut character_list,
|
||||
if c1_player.is_some() {
|
||||
*c1_transform
|
||||
} else if c2_player.is_some() {
|
||||
*c2_transform
|
||||
} else {
|
||||
Transform::identity().with_translation(
|
||||
(c1_transform.translation + c2_transform.translation) * 0.5,
|
||||
)
|
||||
},
|
||||
new_color.into(),
|
||||
c1_player.is_some() || c2_player.is_some(),
|
||||
);
|
||||
|
||||
audio_system::play_audio(&audio_assets.warp_notes, &audio, new_color, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn char_platform_collision_event_system(
|
||||
mut collision_events: EventReader<CollisionEvent>,
|
||||
mut platform_count_query: Query<&mut PlatformCount>,
|
||||
platform_query: Query<&Platform>,
|
||||
) {
|
||||
// detect platform + player collisions only
|
||||
for collision_event in collision_events.iter() {
|
||||
match collision_event {
|
||||
CollisionEvent::Started(e1, e2, flags) => {
|
||||
if *flags == CollisionEventFlags::SENSOR {
|
||||
if let (Ok(mut platform_count), Ok(_)) =
|
||||
(platform_count_query.get_mut(*e1), platform_query.get(*e2))
|
||||
{
|
||||
platform_count.increment();
|
||||
} else if let (Ok(mut platform_count), Ok(_)) =
|
||||
(platform_count_query.get_mut(*e2), platform_query.get(*e1))
|
||||
{
|
||||
platform_count.increment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollisionEvent::Stopped(e1, e2, flags) => {
|
||||
if *flags == CollisionEventFlags::SENSOR {
|
||||
if let (Ok(mut platform_count), Ok(_)) =
|
||||
(platform_count_query.get_mut(*e1), platform_query.get(*e2))
|
||||
{
|
||||
platform_count.decrement();
|
||||
} else if let (Ok(mut platform_count), Ok(_)) =
|
||||
(platform_count_query.get_mut(*e2), platform_query.get(*e1))
|
||||
{
|
||||
platform_count.decrement();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.insert(Level);
|
||||
}
|
||||
|
||||
fn collision_event_system(
|
||||
mut commands: Commands,
|
||||
character_meshes: Res<CharacterMeshes>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
mut collision_events: EventReader<CollisionEvent>,
|
||||
mut character_query: Query<(
|
||||
|
@ -397,25 +229,52 @@ fn collision_event_system(
|
|||
Option<&Player>,
|
||||
)>,
|
||||
pass_through_filter_query: Query<&PassThroughFilter>,
|
||||
melty_query: Query<&Melty>,
|
||||
mut character_list: ResMut<CharacterList>,
|
||||
mut change_character_event: EventWriter<ChangeCharacterEvent>,
|
||||
mut collision_counter_query: Query<&mut CollisionCount>,
|
||||
mut app_state: ResMut<State<AppState>>,
|
||||
audio: Res<crossbeam_channel::Sender<AudioMsg>>,
|
||||
) {
|
||||
for collision_event in collision_events.iter() {
|
||||
if let CollisionEvent::Started(e1, e2, flags) = collision_event {
|
||||
match collision_event {
|
||||
CollisionEvent::Started(e1, e2, flags) => {
|
||||
if flags.is_empty() {
|
||||
if let (Ok((c_color, _c_transform, _c_material, _c_player)), Ok(melty)) =
|
||||
(character_query.get_mut(*e1), melty_query.get(*e2))
|
||||
if let (
|
||||
Ok((c1_color, c1_transform, _c1_material, c1_player)),
|
||||
Ok((c2_color, c2_transform, _c2_material, c2_player)),
|
||||
) = (character_query.get(*e1), character_query.get(*e2))
|
||||
{
|
||||
if (Vec4::from(melty.0) - Vec4::from(c_color.0)).max_element() <= 0. {
|
||||
commands.entity(*e2).despawn_recursive();
|
||||
}
|
||||
} else if let (Ok((c_color, _c_transform, _c_material, _c_player)), Ok(melty)) =
|
||||
(character_query.get_mut(*e2), melty_query.get(*e1))
|
||||
{
|
||||
if (Vec4::from(melty.0) - Vec4::from(c_color.0)).max_element() <= 0. {
|
||||
commands.entity(*e1).despawn_recursive();
|
||||
commands.entity(*e2).despawn_recursive();
|
||||
|
||||
let new_color = (Vec4::from(c1_color.0) + Vec4::from(c2_color.0))
|
||||
.clamp(Vec4::ZERO, Vec4::ONE);
|
||||
|
||||
// If color approximately white
|
||||
if app_state.current() == &AppState::Game
|
||||
&& 4. - new_color.length_squared() < 0.1
|
||||
{
|
||||
app_state.replace(AppState::Win).ok();
|
||||
}
|
||||
|
||||
// position character based on current player location
|
||||
spawn_character(
|
||||
&mut commands,
|
||||
&character_meshes,
|
||||
&mut materials,
|
||||
&audio,
|
||||
if c1_player.is_some() {
|
||||
*c1_transform
|
||||
} else if c2_player.is_some() {
|
||||
*c2_transform
|
||||
} else {
|
||||
Transform::identity().with_translation(
|
||||
(c1_transform.translation + c2_transform.translation) / 2.,
|
||||
)
|
||||
},
|
||||
new_color.into(),
|
||||
c1_player.is_some() || c2_player.is_some(),
|
||||
);
|
||||
|
||||
audio.send(AudioMsg::Fusion).ok();
|
||||
}
|
||||
} else if *flags == CollisionEventFlags::SENSOR {
|
||||
if let (Ok((mut c_color, _c_transform, mut c_material, c_player)), Ok(filter)) = (
|
||||
|
@ -423,14 +282,17 @@ fn collision_event_system(
|
|||
pass_through_filter_query.get(*e2),
|
||||
) {
|
||||
c_color.0 = filter.apply(c_color.0);
|
||||
if c_color.0.as_hsla_f32()[2] < 0.1 {
|
||||
commands.entity(*e1).despawn_recursive();
|
||||
character_list.0.remove(e1);
|
||||
if c_player.is_some() {
|
||||
change_character_event.send(ChangeCharacterEvent);
|
||||
}
|
||||
}
|
||||
*c_material = materials.add(ColorMaterial::from(c_color.0));
|
||||
|
||||
if c_player.is_some() {
|
||||
audio
|
||||
.send(AudioMsg::Color([
|
||||
c_color.0.r(),
|
||||
c_color.0.g(),
|
||||
c_color.0.b(),
|
||||
]))
|
||||
.ok();
|
||||
}
|
||||
} else if let (
|
||||
Ok((mut c_color, _c_transform, mut c_material, c_player)),
|
||||
Ok(filter),
|
||||
|
@ -439,14 +301,43 @@ fn collision_event_system(
|
|||
pass_through_filter_query.get(*e1),
|
||||
) {
|
||||
c_color.0 = filter.apply(c_color.0);
|
||||
if c_color.0.as_hsla_f32()[2] < 0.1 {
|
||||
commands.entity(*e2).despawn_recursive();
|
||||
character_list.0.remove(e2);
|
||||
if c_player.is_some() {
|
||||
change_character_event.send(ChangeCharacterEvent);
|
||||
}
|
||||
}
|
||||
*c_material = materials.add(ColorMaterial::from(c_color.0));
|
||||
|
||||
if c_player.is_some() {
|
||||
audio
|
||||
.send(AudioMsg::Color([
|
||||
c_color.0.r(),
|
||||
c_color.0.g(),
|
||||
c_color.0.b(),
|
||||
]))
|
||||
.ok();
|
||||
}
|
||||
} else if let (Ok(mut collision_count), Err(_)) = (
|
||||
collision_counter_query.get_mut(*e1),
|
||||
character_query.get_mut(*e2),
|
||||
) {
|
||||
collision_count.0 += 1;
|
||||
} else if let (Ok(mut collision_count), Err(_)) = (
|
||||
collision_counter_query.get_mut(*e2),
|
||||
character_query.get_mut(*e1),
|
||||
) {
|
||||
collision_count.0 += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
CollisionEvent::Stopped(e1, e2, flags) => {
|
||||
if *flags == CollisionEventFlags::SENSOR {
|
||||
if let (Ok(mut collision_count), Err(_)) = (
|
||||
collision_counter_query.get_mut(*e1),
|
||||
character_query.get_mut(*e2),
|
||||
) {
|
||||
collision_count.0 = collision_count.0.saturating_sub(1);
|
||||
} else if let (Ok(mut collision_count), Err(_)) = (
|
||||
collision_counter_query.get_mut(*e2),
|
||||
character_query.get_mut(*e1),
|
||||
) {
|
||||
collision_count.0 = collision_count.0.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -455,58 +346,67 @@ fn collision_event_system(
|
|||
|
||||
fn change_character_system(
|
||||
mut commands: Commands,
|
||||
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
characters: Query<(Entity, &CharacterColor, Option<&Player>)>,
|
||||
character_list: Res<CharacterList>,
|
||||
audio_assets: Res<audio_system::AudioAssets>,
|
||||
audio: Res<Audio>,
|
||||
change_character_event: EventReader<ChangeCharacterEvent>,
|
||||
audio: Res<crossbeam_channel::Sender<AudioMsg>>,
|
||||
) {
|
||||
if !keyboard_input.just_pressed(KeyCode::Tab) && change_character_event.is_empty() {
|
||||
if !keyboard_input.just_pressed(KeyCode::Tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some((player_entity, _color, _)) = characters
|
||||
.iter()
|
||||
.find(|(_entity, _color, player)| player.is_some())
|
||||
.or_else(|| characters.iter().next())
|
||||
{
|
||||
commands.entity(player_entity).remove::<Player>();
|
||||
if let Some(new_player_entity) = character_list
|
||||
.0
|
||||
.range(player_entity..)
|
||||
.nth(1)
|
||||
.or_else(|| character_list.0.iter().next())
|
||||
{
|
||||
commands.entity(*new_player_entity).insert(Player);
|
||||
if let Ok((_entity, color, _player)) = characters.get(*new_player_entity) {
|
||||
audio_system::play_audio(&audio_assets.reverb_notes, &audio, color.0.into(), 1.0);
|
||||
let mut player_idx: usize = 0;
|
||||
let mut player_count: usize = 0;
|
||||
|
||||
// find player idx
|
||||
for (_entity, _color, player) in characters.iter() {
|
||||
if player.is_some() {
|
||||
player_idx = player_count;
|
||||
}
|
||||
player_count += 1;
|
||||
}
|
||||
|
||||
// calculate next player index
|
||||
let next_player_idx = (player_idx + 1) % player_count;
|
||||
player_count = 0;
|
||||
|
||||
// exchange `Player` component from old `player_idx` to new `next_player_idx`
|
||||
for (entity, color, _player) in characters.iter() {
|
||||
if player_count == player_idx {
|
||||
commands.entity(entity).remove::<Player>();
|
||||
}
|
||||
|
||||
if player_count == next_player_idx {
|
||||
commands.entity(entity).insert(Player);
|
||||
audio
|
||||
.send(AudioMsg::Color([color.0.r(), color.0.g(), color.0.b()]))
|
||||
.ok();
|
||||
audio.send(AudioMsg::Switch).ok();
|
||||
}
|
||||
|
||||
player_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn player_movement_system(
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut characters: Query<(&mut Velocity, &Children, &CharacterColor), With<Player>>,
|
||||
mut platform_count_query: Query<&mut PlatformCount>,
|
||||
audio_assets: Res<audio_system::AudioAssets>,
|
||||
audio: Res<Audio>,
|
||||
mut characters: Query<(&mut Velocity, &Children), With<Player>>,
|
||||
collision_counter_query: Query<&CollisionCount>,
|
||||
audio: Res<crossbeam_channel::Sender<AudioMsg>>,
|
||||
) {
|
||||
let right_pressed: bool =
|
||||
keyboard_input.pressed(KeyCode::Right) || keyboard_input.pressed(KeyCode::D);
|
||||
let left_pressed: bool =
|
||||
keyboard_input.pressed(KeyCode::Left) || keyboard_input.pressed(KeyCode::A);
|
||||
|
||||
for (mut velocity, children, color) in characters.iter_mut() {
|
||||
for (mut velocity, children) in characters.iter_mut() {
|
||||
velocity.linvel.x = 200. * (right_pressed as i8 - left_pressed as i8) as f32;
|
||||
|
||||
let mut platform_count: Mut<PlatformCount> =
|
||||
platform_count_query.get_mut(children[0]).unwrap();
|
||||
if keyboard_input.just_pressed(KeyCode::Space) && platform_count.is_landed() {
|
||||
audio_system::play_audio(&audio_assets.notes, &audio, color.0.into(), 0.5);
|
||||
if keyboard_input.just_pressed(KeyCode::Space)
|
||||
&& collision_counter_query.get(children[0]).unwrap().0 != 0
|
||||
{
|
||||
audio.send(AudioMsg::Jump).ok();
|
||||
velocity.linvel.y = 700.;
|
||||
platform_count.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -540,8 +440,7 @@ fn win_setup(
|
|||
transform: Transform::from_xyz(0., 0., 3.),
|
||||
..default()
|
||||
})
|
||||
.insert(Level)
|
||||
.insert(WinText);
|
||||
.insert(Level);
|
||||
commands
|
||||
.spawn_bundle(Text2dBundle {
|
||||
text: Text::from_section(
|
||||
|
@ -556,26 +455,19 @@ fn win_setup(
|
|||
transform: Transform::from_xyz(0., 0., 4.),
|
||||
..Default::default()
|
||||
})
|
||||
.insert(Level)
|
||||
.insert(WinText);
|
||||
.insert(Level);
|
||||
}
|
||||
|
||||
fn camera_system(
|
||||
mut camera_query: Query<(&Camera, &mut OrthographicProjection, &mut Transform)>,
|
||||
fn move_camera(
|
||||
mut camera_query: Query<(&Camera, &mut Transform)>,
|
||||
characters: Query<&Transform, (Without<Camera>, With<Player>)>,
|
||||
time: Res<Time>,
|
||||
mut zoom_timer: ResMut<levels::ZoomTimer>,
|
||||
) {
|
||||
fn lerp(x: f32, y: f32, t: f32) -> f32 {
|
||||
((y - x) * t) + x
|
||||
}
|
||||
|
||||
const MARGIN: f32 = 300.0;
|
||||
const FOLLOW_SPEED: f32 = std::f32::consts::PI;
|
||||
const ZOOM_SPEED: f32 = std::f32::consts::E;
|
||||
|
||||
for character_transform in characters.iter() {
|
||||
let (camera, mut projection, mut camera_transform) = camera_query.single_mut();
|
||||
let (camera, mut camera_transform) = camera_query.single_mut();
|
||||
|
||||
let size: Vec2 = camera.logical_viewport_size().unwrap();
|
||||
let half_height: f32 = size.y * 0.5;
|
||||
|
@ -583,44 +475,24 @@ fn camera_system(
|
|||
// prevent camera from going too low
|
||||
target_translation.y = target_translation.y.max(half_height - MARGIN);
|
||||
|
||||
camera_transform.translation = camera_transform
|
||||
.translation
|
||||
.lerp(target_translation, time.delta_seconds() * FOLLOW_SPEED);
|
||||
|
||||
if zoom_timer.0.finished() {
|
||||
projection.scale = lerp(projection.scale, 1.0, time.delta_seconds() * ZOOM_SPEED);
|
||||
} else {
|
||||
projection.scale = 3.0;
|
||||
camera_transform.translation = Vec3::ZERO;
|
||||
zoom_timer.0.tick(time.delta());
|
||||
}
|
||||
camera_transform.translation = camera_transform.translation.lerp(
|
||||
target_translation,
|
||||
time.delta_seconds() * FOLLOW_SPEED,
|
||||
);
|
||||
|
||||
// always make sure that camera is away from the object in order to render them
|
||||
camera_transform.translation.z = 999.0;
|
||||
}
|
||||
}
|
||||
|
||||
fn move_win_text_system(
|
||||
camera_query: Query<&Transform, With<Camera>>,
|
||||
mut win_text_query: Query<&mut Transform, (With<WinText>, Without<Camera>)>,
|
||||
) {
|
||||
let camera_pos = camera_query.single();
|
||||
for mut pos in win_text_query.iter_mut() {
|
||||
pos.translation.x = camera_pos.translation.x;
|
||||
pos.translation.y = camera_pos.translation.y;
|
||||
}
|
||||
}
|
||||
|
||||
fn level_keyboard_system(
|
||||
mut commands: Commands,
|
||||
mut current_level: ResMut<CurrentLevel>,
|
||||
mut level_startup_event: EventWriter<LevelStartupEvent>,
|
||||
mut camera_query: Query<&mut Transform, With<Camera>>,
|
||||
keyboard_input: Res<Input<KeyCode>>,
|
||||
mut character_list: ResMut<CharacterList>,
|
||||
level_query: Query<Entity, With<Level>>,
|
||||
mut app_state: ResMut<State<AppState>>,
|
||||
mut zoom_timer: ResMut<levels::ZoomTimer>,
|
||||
) {
|
||||
if app_state.current() == &AppState::Win && keyboard_input.just_pressed(KeyCode::Return) {
|
||||
current_level.0 = Some(LevelId(
|
||||
|
@ -630,33 +502,11 @@ fn level_keyboard_system(
|
|||
}
|
||||
|
||||
if keyboard_input.just_pressed(KeyCode::R) {
|
||||
character_list.0.clear();
|
||||
for entity in level_query.iter() {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
if app_state.replace(AppState::Game).is_err() {
|
||||
crate::levels::setup_level(
|
||||
&mut level_startup_event,
|
||||
&mut camera_query,
|
||||
&mut zoom_timer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn kill_character_system(
|
||||
mut commands: Commands,
|
||||
character_query: Query<(Entity, &Transform, Option<&Player>), With<CharacterColor>>,
|
||||
mut character_list: ResMut<CharacterList>,
|
||||
mut change_character_event: EventWriter<ChangeCharacterEvent>,
|
||||
) {
|
||||
for (entity, transform, player) in character_query.iter() {
|
||||
if transform.translation.y < -512. {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
character_list.0.remove(&entity);
|
||||
if player.is_some() {
|
||||
change_character_event.send(ChangeCharacterEvent);
|
||||
}
|
||||
crate::levels::setup_level(&mut level_startup_event, &mut camera_query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
194
src/levels.rs
194
src/levels.rs
|
@ -1,42 +1,20 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub use stored::*;
|
||||
|
||||
use crate::game::*;
|
||||
|
||||
use bevy::{prelude::*, reflect::TypeUuid};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct ZoomTimer(pub Timer);
|
||||
|
||||
impl Default for ZoomTimer {
|
||||
fn default() -> Self {
|
||||
Self(Timer::new(Duration::new(2, 0), false))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_level(
|
||||
level_startup_event: &mut EventWriter<LevelStartupEvent>,
|
||||
camera_query: &mut Query<&mut Transform, With<Camera>>,
|
||||
zoom_timer: &mut ResMut<ZoomTimer>,
|
||||
) {
|
||||
let mut transform = camera_query.single_mut();
|
||||
transform.translation = Default::default();
|
||||
|
||||
zoom_timer.0.reset();
|
||||
camera_query.single_mut().translation = Default::default();
|
||||
|
||||
level_startup_event.send(LevelStartupEvent);
|
||||
}
|
||||
|
||||
pub fn despawn_level(
|
||||
mut commands: Commands,
|
||||
mut character_list: ResMut<CharacterList>,
|
||||
level_query: Query<Entity, With<Level>>,
|
||||
) {
|
||||
character_list.0.clear();
|
||||
pub fn despawn_level(mut commands: Commands, level_query: Query<Entity, With<Level>>) {
|
||||
for entity in level_query.iter() {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
|
@ -51,7 +29,7 @@ pub fn post_setup_level(
|
|||
current_level: Res<CurrentLevel>,
|
||||
mut level_startup_event: EventReader<LevelStartupEvent>,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut character_list: ResMut<CharacterList>,
|
||||
audio: Res<crossbeam_channel::Sender<AudioMsg>>,
|
||||
stored_levels_assets: Res<Assets<StoredLevels>>,
|
||||
stored_levels_handle: Res<Handle<StoredLevels>>,
|
||||
) {
|
||||
|
@ -69,7 +47,7 @@ pub fn post_setup_level(
|
|||
&mut meshes,
|
||||
&mut materials,
|
||||
&asset_server,
|
||||
&mut character_list,
|
||||
&audio,
|
||||
stored_level,
|
||||
);
|
||||
}
|
||||
|
@ -77,94 +55,10 @@ pub fn post_setup_level(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn spawn_stored_level(
|
||||
commands: &mut Commands,
|
||||
character_meshes: &Res<CharacterMeshes>,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
character_list: &mut ResMut<CharacterList>,
|
||||
|
||||
stored_level: &StoredLevel,
|
||||
) {
|
||||
let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
|
||||
spawn_platforms(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
stored_level.platforms.iter().map(|platform| {
|
||||
(
|
||||
Transform::from_xyz(platform.pos.x, platform.pos.y, 0.),
|
||||
platform.size,
|
||||
)
|
||||
}),
|
||||
);
|
||||
spawn_characters(
|
||||
commands,
|
||||
character_meshes,
|
||||
materials,
|
||||
character_list,
|
||||
stored_level.characters.iter().map(|character| {
|
||||
(
|
||||
Transform::from_xyz(character.pos.x, character.pos.y, 0.),
|
||||
character.color.into(),
|
||||
)
|
||||
}),
|
||||
);
|
||||
for absorbing_filter in stored_level.absorbing_filters.iter() {
|
||||
spawn_absorbing_filter(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
Transform::from_xyz(absorbing_filter.pos.x, absorbing_filter.pos.y, 2.),
|
||||
absorbing_filter.size,
|
||||
absorbing_filter.color.into(),
|
||||
);
|
||||
}
|
||||
for rotating_filter in stored_level.rotating_filters.iter() {
|
||||
spawn_rotating_filter(
|
||||
commands,
|
||||
asset_server,
|
||||
Transform::from_xyz(rotating_filter.pos.x, rotating_filter.pos.y, 2.),
|
||||
rotating_filter.angle,
|
||||
);
|
||||
}
|
||||
for melty_platform in stored_level.melty_platforms.iter() {
|
||||
spawn_melty_platform(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
asset_server,
|
||||
Transform::from_xyz(melty_platform.pos.x, melty_platform.pos.y, 2.),
|
||||
melty_platform.color.into(),
|
||||
);
|
||||
}
|
||||
for text in stored_level.texts.iter() {
|
||||
commands
|
||||
.spawn_bundle(Text2dBundle {
|
||||
text: Text::from_section(
|
||||
&text.text,
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: text.font_size,
|
||||
color: Color::WHITE,
|
||||
},
|
||||
)
|
||||
.with_alignment(TextAlignment::CENTER),
|
||||
transform: Transform::from_xyz(text.pos.x, text.pos.y, 0.),
|
||||
..Default::default()
|
||||
})
|
||||
.insert(Level);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod stored {
|
||||
use super::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, TypeUuid)]
|
||||
#[uuid = "1fbba930-644b-0d62-2514-4b302b945327"]
|
||||
pub struct StoredLevels {
|
||||
pub levels: Vec<StoredLevel>,
|
||||
levels: Vec<StoredLevel>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TypeUuid)]
|
||||
|
@ -175,7 +69,6 @@ pub mod stored {
|
|||
pub platforms: Vec<StoredPlatform>,
|
||||
pub absorbing_filters: Vec<StoredAbsorbingFilter>,
|
||||
pub rotating_filters: Vec<StoredRotatingFilter>,
|
||||
pub melty_platforms: Vec<StoredMeltyPlatform>,
|
||||
pub texts: Vec<StoredText>,
|
||||
}
|
||||
|
||||
|
@ -208,13 +101,6 @@ pub mod stored {
|
|||
pub angle: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TypeUuid)]
|
||||
#[uuid = "cb0773ef-eca6-9b96-dcba-f4240ebdcf40"]
|
||||
pub struct StoredMeltyPlatform {
|
||||
pub pos: Vec2,
|
||||
pub color: Vec4,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TypeUuid)]
|
||||
#[uuid = "72f6321a-f01f-6eea-9b17-3159837a2fd3"]
|
||||
pub struct StoredText {
|
||||
|
@ -222,4 +108,74 @@ pub mod stored {
|
|||
pub font_size: f32,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub fn spawn_stored_level(
|
||||
commands: &mut Commands,
|
||||
character_meshes: &Res<CharacterMeshes>,
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: &Res<AssetServer>,
|
||||
audio: &Res<crossbeam_channel::Sender<AudioMsg>>,
|
||||
|
||||
stored_level: &StoredLevel,
|
||||
) {
|
||||
let font = asset_server.get_handle("UacariLegacy-Thin.ttf");
|
||||
spawn_platforms(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
stored_level.platforms.iter().map(|platform| {
|
||||
(
|
||||
Transform::from_xyz(platform.pos.x, platform.pos.y, 0.),
|
||||
platform.size,
|
||||
)
|
||||
}),
|
||||
);
|
||||
spawn_characters(
|
||||
commands,
|
||||
character_meshes,
|
||||
materials,
|
||||
audio,
|
||||
stored_level.characters.iter().map(|character| {
|
||||
(
|
||||
Transform::from_xyz(character.pos.x, character.pos.y, 0.),
|
||||
character.color.into(),
|
||||
)
|
||||
}),
|
||||
);
|
||||
for absorbing_filter in stored_level.absorbing_filters.iter() {
|
||||
spawn_absorbing_filter(
|
||||
commands,
|
||||
meshes,
|
||||
materials,
|
||||
Transform::from_xyz(absorbing_filter.pos.x, absorbing_filter.pos.y, 2.),
|
||||
absorbing_filter.size,
|
||||
absorbing_filter.color.into(),
|
||||
);
|
||||
}
|
||||
for rotating_filter in stored_level.rotating_filters.iter() {
|
||||
spawn_rotating_filter(
|
||||
commands,
|
||||
asset_server,
|
||||
Transform::from_xyz(rotating_filter.pos.x, rotating_filter.pos.y, 2.),
|
||||
rotating_filter.angle,
|
||||
);
|
||||
}
|
||||
for text in stored_level.texts.iter() {
|
||||
commands
|
||||
.spawn_bundle(Text2dBundle {
|
||||
text: Text::from_section(
|
||||
&text.text,
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: text.font_size,
|
||||
color: Color::WHITE,
|
||||
},
|
||||
)
|
||||
.with_alignment(TextAlignment::CENTER),
|
||||
transform: Transform::from_xyz(text.pos.x, text.pos.y, 0.),
|
||||
..Default::default()
|
||||
})
|
||||
.insert(Level);
|
||||
}
|
||||
}
|
||||
|
|
135
src/main.rs
135
src/main.rs
|
@ -1,9 +1,5 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod editor;
|
||||
|
||||
mod audio_system;
|
||||
mod audio;
|
||||
mod filters;
|
||||
mod game;
|
||||
mod levels;
|
||||
|
@ -11,101 +7,81 @@ mod menu;
|
|||
mod particle_effect;
|
||||
|
||||
use bevy::{
|
||||
asset::{Asset, HandleId, LoadState},
|
||||
prelude::*,
|
||||
window::{WindowMode, WindowResizeConstraints},
|
||||
window::{WindowId, WindowMode},
|
||||
};
|
||||
use bevy_common_assets::json::JsonAssetPlugin;
|
||||
use bevy_rapier2d::prelude::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::thread;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_thread as thread;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
enum AppState {
|
||||
Loading,
|
||||
Menu,
|
||||
Game,
|
||||
Win,
|
||||
Editor,
|
||||
}
|
||||
|
||||
struct UseEditor(bool);
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
struct LoadingAssets(Vec<HandleId>);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(start)] pub fn dummy_main() {}
|
||||
|
||||
impl LoadingAssets {
|
||||
fn add<T: Asset>(&mut self, handle: Handle<T>) -> Handle<T> {
|
||||
self.0.push(handle.id);
|
||||
handle
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
pub fn run() {
|
||||
main();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let (first_level, use_editor) = {
|
||||
let mut args = std::env::args().skip(1);
|
||||
(
|
||||
game::LevelId(args.next().map_or(0, |s| s.parse().unwrap_or(0))),
|
||||
args.next().map_or(false, |s| s == "e"),
|
||||
)
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let (first_level, use_editor) = (game::LevelId(0), false);
|
||||
let (audio_event_sender, audio_event_receiver) =
|
||||
crossbeam_channel::bounded::<game::AudioMsg>(512);
|
||||
|
||||
let mut app = App::new();
|
||||
app.insert_resource(Msaa { samples: 4 })
|
||||
.insert_resource(WindowDescriptor {
|
||||
width: 640.0,
|
||||
height: 480.0,
|
||||
resize_constraints: WindowResizeConstraints {
|
||||
min_width: 256.,
|
||||
min_height: 256.,
|
||||
max_width: f32::INFINITY,
|
||||
max_height: f32::INFINITY,
|
||||
},
|
||||
resizable: true,
|
||||
title: "Lux synthesĕ".into(),
|
||||
..Default::default()
|
||||
})
|
||||
.insert_resource(UseEditor(use_editor))
|
||||
.add_state(AppState::Loading)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
thread::spawn(move || audio::setup(audio_event_receiver));
|
||||
thread::spawn(|| {/* ... */});
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let first_level = game::LevelId(
|
||||
std::env::args()
|
||||
.nth(1)
|
||||
.map_or(0, |s| s.parse().unwrap_or(0)),
|
||||
);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let first_level = game::LevelId(0);
|
||||
|
||||
App::new()
|
||||
.insert_resource(Msaa { samples: 4 })
|
||||
.insert_resource(audio_event_sender)
|
||||
.add_state(AppState::Menu)
|
||||
.insert_resource(game::FirstLevel(first_level))
|
||||
.insert_resource(ClearColor(Color::BLACK))
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugin(audio_system::AudioSystemPlugin)
|
||||
//.add_plugin(RapierDebugRenderPlugin::default())
|
||||
// .add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new())
|
||||
.add_plugin(JsonAssetPlugin::<levels::StoredLevels>::new(&[
|
||||
"levels.json",
|
||||
]));
|
||||
|
||||
if !use_editor {
|
||||
app.add_plugin(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(64.0))
|
||||
]))
|
||||
.add_plugin(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(64.0))
|
||||
//.add_plugin(RapierDebugRenderPlugin::default())
|
||||
.add_plugin(menu::MenuPlugin)
|
||||
.add_plugin(game::GamePlugin)
|
||||
.add_plugin(particle_effect::ParticleEffectPlugin);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if use_editor {
|
||||
app.add_plugin(editor::EditorPlugin);
|
||||
}
|
||||
|
||||
app.add_system(keyboard_util_system)
|
||||
.add_plugin(particle_effect::ParticleEffectPlugin)
|
||||
//.add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new())
|
||||
.add_system(keyboard_util_system)
|
||||
.add_startup_system(setup)
|
||||
.add_system_set(SystemSet::on_update(AppState::Loading).with_system(loading_system))
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
let mut assets = LoadingAssets(Vec::new());
|
||||
commands.insert_resource(
|
||||
assets.add(asset_server.load::<levels::StoredLevels, _>("game.levels.json")),
|
||||
);
|
||||
commands.insert_resource(assets.add(asset_server.load::<Font, _>("UacariLegacy-Thin.ttf")));
|
||||
commands.insert_resource([
|
||||
assets.add(asset_server.load::<Image, _>("bevy.png")),
|
||||
assets.add(asset_server.load("melty.png")),
|
||||
]);
|
||||
commands.insert_resource(assets);
|
||||
fn setup(mut commands: Commands, mut windows: ResMut<Windows>, asset_server: Res<AssetServer>) {
|
||||
windows
|
||||
.get_mut(WindowId::primary())
|
||||
.unwrap()
|
||||
.set_title(String::from("Bevyjam"));
|
||||
|
||||
commands.insert_resource(asset_server.load::<levels::StoredLevels, _>("game.levels.json"));
|
||||
commands.insert_resource(asset_server.load::<Font, _>("UacariLegacy-Thin.ttf"));
|
||||
commands.insert_resource(asset_server.load::<Image, _>("bevy.png"));
|
||||
|
||||
commands.spawn_bundle(Camera2dBundle::default());
|
||||
commands.insert_resource(AmbientLight {
|
||||
|
@ -114,23 +90,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
});
|
||||
}
|
||||
|
||||
fn loading_system(
|
||||
asset_server: Res<AssetServer>,
|
||||
use_editor: Res<UseEditor>,
|
||||
assets: Res<LoadingAssets>,
|
||||
mut app_state: ResMut<State<AppState>>,
|
||||
) {
|
||||
if asset_server.get_group_load_state(assets.0.iter().copied()) == LoadState::Loaded {
|
||||
app_state
|
||||
.replace(if use_editor.0 {
|
||||
AppState::Editor
|
||||
} else {
|
||||
AppState::Menu
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_util_system(keyboard_input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
|
|
|
@ -23,7 +23,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||
commands
|
||||
.spawn_bundle(Text2dBundle {
|
||||
text: Text::from_section(
|
||||
"Lux synthesĕ",
|
||||
"BEVYJAM",
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: 96.0,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use bevy::{prelude::*, sprite::Mesh2dHandle};
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use rand::Rng;
|
||||
use rand_distr::{Distribution, UnitCircle};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
@ -44,6 +44,7 @@ impl FromWorld for ParticleMesh {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(bevy_inspector_egui::Inspectable)]
|
||||
pub struct ParticleEffectResource {
|
||||
pub translation: Vec3,
|
||||
pub prev_translation: Vec3,
|
||||
|
@ -68,20 +69,15 @@ pub struct ParticleComponent {
|
|||
}
|
||||
|
||||
impl ParticleComponent {
|
||||
pub fn new(rng: &mut ThreadRng) -> Self {
|
||||
pub fn new() -> Self {
|
||||
let mut particle_component: Self = Self::default();
|
||||
particle_component.randomize_velocity(rng, MIN_VELOCITY, MAX_VELOCITY);
|
||||
particle_component.randomize_velocity(MIN_VELOCITY, MAX_VELOCITY);
|
||||
particle_component
|
||||
}
|
||||
|
||||
pub fn randomize_velocity(
|
||||
&mut self,
|
||||
rng: &mut ThreadRng,
|
||||
min_velocity: f32,
|
||||
max_velocity: f32,
|
||||
) {
|
||||
let random_direction: [f32; 2] = UnitCircle.sample(rng);
|
||||
let random_magnitude: f32 = rng.gen_range(min_velocity..max_velocity);
|
||||
pub fn randomize_velocity(&mut self, min_velocity: f32, max_velocity: f32) {
|
||||
let random_direction: [f32; 2] = UnitCircle.sample(&mut rand::thread_rng());
|
||||
let random_magnitude: f32 = rand::thread_rng().gen_range(min_velocity..max_velocity);
|
||||
self.velocity = Vec3::new(random_direction[0], random_direction[1], 0.0) * random_magnitude;
|
||||
}
|
||||
}
|
||||
|
@ -92,8 +88,6 @@ fn particle_effect_startup(
|
|||
particle_mesh: Res<ParticleMesh>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for _p in 0..POOL_COUNT {
|
||||
let color_mesh = ColorMesh2dBundle {
|
||||
mesh: particle_mesh.square.clone(),
|
||||
|
@ -103,7 +97,7 @@ fn particle_effect_startup(
|
|||
|
||||
commands
|
||||
.spawn_bundle(color_mesh)
|
||||
.insert(ParticleComponent::new(&mut rng));
|
||||
.insert(ParticleComponent::new());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,8 +111,6 @@ fn particle_effect_system(
|
|||
mut particle_effect: ResMut<ParticleEffectResource>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let delta_seconds: f32 = f32::max(0.001, time.delta_seconds());
|
||||
let delta_position: Vec3 = particle_effect.translation - particle_effect.prev_translation;
|
||||
// let overall_velocity: Vec3 = delta_position / delta_seconds;
|
||||
|
@ -133,7 +125,7 @@ fn particle_effect_system(
|
|||
.distance_squared(particle_effect.translation);
|
||||
if squared_distance > particle_effect.radius_squared {
|
||||
transform.translation = particle_effect.translation;
|
||||
particle_component.randomize_velocity(&mut rng, MIN_VELOCITY, MAX_VELOCITY);
|
||||
particle_component.randomize_velocity(MIN_VELOCITY, MAX_VELOCITY);
|
||||
}
|
||||
|
||||
if let Some(material) = materials.get_mut(color_material) {
|
||||
|
@ -143,6 +135,5 @@ fn particle_effect_system(
|
|||
/ particle_effect.radius_squared,
|
||||
);
|
||||
}
|
||||
transform.translation.z = 0.005;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue