Add(yuno): base project.
This commit is contained in:
commit
ff9950a89d
22 changed files with 5228 additions and 0 deletions
6
.cargo/config.toml
Normal file
6
.cargo/config.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "clang"
|
||||||
|
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold", "-Zshare-generics=y"]
|
||||||
|
|
||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
runner = "wasm-server-runner"
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/dist
|
11
.woodpecker/clippy.yml
Normal file
11
.woodpecker/clippy.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
when:
|
||||||
|
- event: [pull_request]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
clippy:
|
||||||
|
image: git.ragarock.moe/silvana/yuno/rust:latest
|
||||||
|
environment: [CARGO_TERM_COLOR=always, CARGO_HOME=./.cargo-home]
|
||||||
|
commands:
|
||||||
|
- rustup default nightly
|
||||||
|
- rustup component add clippy
|
||||||
|
- cargo clippy -- -D warnings
|
11
.woodpecker/fmt.yml
Normal file
11
.woodpecker/fmt.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
when:
|
||||||
|
- event: [pull_request]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
fmt:
|
||||||
|
image: git.ragarock.moe/silvana/yuno/rust:latest
|
||||||
|
environment: [CARGO_TERM_COLOR=always, CARGO_HOME=./.cargo-home]
|
||||||
|
commands:
|
||||||
|
- rustup default nightly
|
||||||
|
- rustup component add rustfmt
|
||||||
|
- cargo fmt -- --check
|
11
.woodpecker/test.yml
Normal file
11
.woodpecker/test.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
when:
|
||||||
|
- event: [pull_request]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
test:
|
||||||
|
image: git.ragarock.moe/silvana/yuno/rust:latest
|
||||||
|
environment: [CARGO_TERM_COLOR=always, CARGO_HOME=./.cargo-home]
|
||||||
|
commands:
|
||||||
|
- rustup default nightly
|
||||||
|
- cargo check
|
||||||
|
- cargo test
|
4544
Cargo.lock
generated
Normal file
4544
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
73
Cargo.toml
Normal file
73
Cargo.toml
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
[package]
|
||||||
|
name = "yuno"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
exclude = ["dist", "build", "assets", "credits"]
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
# This is used by trunk as it doesn't support custom profiles: https://github.com/trunk-rs/trunk/issues/605
|
||||||
|
# xbuild also uses this profile for building android AABs because I couldn't find a configuration for it
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
# Profile for distribution
|
||||||
|
[profile.dist]
|
||||||
|
inherits = "release"
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
dev = ["bevy/dynamic_linking"]
|
||||||
|
|
||||||
|
# All of Bevy's default features exept for the audio related ones (bevy_audio, vorbis), since they clash with bevy_kira_audio
|
||||||
|
# and android_shared_stdcxx, since that is covered in `mobile`
|
||||||
|
[dependencies]
|
||||||
|
bevy = { version = "0.14", default-features = false, features = [
|
||||||
|
"animation",
|
||||||
|
"bevy_asset",
|
||||||
|
"bevy_state",
|
||||||
|
"bevy_color",
|
||||||
|
"bevy_gilrs",
|
||||||
|
"bevy_scene",
|
||||||
|
"bevy_winit",
|
||||||
|
"bevy_core_pipeline",
|
||||||
|
"bevy_pbr",
|
||||||
|
"bevy_gltf",
|
||||||
|
"bevy_render",
|
||||||
|
"bevy_sprite",
|
||||||
|
"bevy_text",
|
||||||
|
"bevy_ui",
|
||||||
|
"multi_threaded",
|
||||||
|
"png",
|
||||||
|
"hdr",
|
||||||
|
"x11",
|
||||||
|
"bevy_gizmos",
|
||||||
|
"tonemapping_luts",
|
||||||
|
"smaa_luts",
|
||||||
|
"default_font",
|
||||||
|
"webgl2",
|
||||||
|
"sysinfo_plugin",
|
||||||
|
] }
|
||||||
|
bevy_kira_audio = { version = "0.20" }
|
||||||
|
bevy_asset_loader = { version = "0.21", features = ["2d"] }
|
||||||
|
rand = { version = "0.8.3" }
|
||||||
|
webbrowser = { version = "1", features = ["hardened"] }
|
||||||
|
|
||||||
|
# keep the following in sync with Bevy's dependencies
|
||||||
|
winit = { version = "0.30", default-features = false }
|
||||||
|
image = { version = "0.25", default-features = false }
|
||||||
|
## This greatly improves WGPU's performance due to its heavy use of trace! calls
|
||||||
|
log = { version = "0.4", features = [
|
||||||
|
"max_level_debug",
|
||||||
|
"release_max_level_warn",
|
||||||
|
] }
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
FROM alpine:edge
|
||||||
|
|
||||||
|
RUN apk add curl build-base mold clang gcc libc-dev pkgconf libx11-dev alsa-lib-dev eudev-dev
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs --output init-rust.sh \
|
||||||
|
&& chmod a+x init-rust.sh \
|
||||||
|
&& ./init-rust.sh -y \
|
||||||
|
&& /root/.cargo/bin/cargo install trunk
|
||||||
|
|
||||||
|
ENV PATH="/root/.cargo/bin:$PATH"
|
BIN
assets/textures/bevy.png
(Stored with Git LFS)
Normal file
BIN
assets/textures/bevy.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/textures/forgejo.png
(Stored with Git LFS)
Normal file
BIN
assets/textures/forgejo.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/textures/icon.png
(Stored with Git LFS)
Normal file
BIN
assets/textures/icon.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/textures/witch.png
(Stored with Git LFS)
Normal file
BIN
assets/textures/witch.png
(Stored with Git LFS)
Normal file
Binary file not shown.
62
html/sound.js
Normal file
62
html/sound.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// Insert hack to make sound autoplay on Chrome as soon as the user interacts with the tab:
|
||||||
|
// https://developers.google.com/web/updates/2018/11/web-audio-autoplay#moving-forward
|
||||||
|
|
||||||
|
// the following function keeps track of all AudioContexts and resumes them on the first user
|
||||||
|
// interaction with the page. If the function is called and all contexts are already running,
|
||||||
|
// it will remove itself from all event listeners.
|
||||||
|
(function () {
|
||||||
|
// An array of all contexts to resume on the page
|
||||||
|
const audioContextList = [];
|
||||||
|
|
||||||
|
// An array of various user interaction events we should listen for
|
||||||
|
const userInputEventNames = [
|
||||||
|
"click",
|
||||||
|
"contextmenu",
|
||||||
|
"auxclick",
|
||||||
|
"dblclick",
|
||||||
|
"mousedown",
|
||||||
|
"mouseup",
|
||||||
|
"pointerup",
|
||||||
|
"touchend",
|
||||||
|
"keydown",
|
||||||
|
"keyup",
|
||||||
|
];
|
||||||
|
|
||||||
|
// A proxy object to intercept AudioContexts and
|
||||||
|
// add them to the array for tracking and resuming later
|
||||||
|
self.AudioContext = new Proxy(self.AudioContext, {
|
||||||
|
construct(target, args) {
|
||||||
|
const result = new target(...args);
|
||||||
|
audioContextList.push(result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// To resume all AudioContexts being tracked
|
||||||
|
function resumeAllContexts(_event) {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
audioContextList.forEach((context) => {
|
||||||
|
if (context.state !== "running") {
|
||||||
|
context.resume();
|
||||||
|
} else {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If all the AudioContexts have now resumed then we unbind all
|
||||||
|
// the event listeners from the page to prevent unnecessary resume attempts
|
||||||
|
// Checking count > 0 ensures that the user interaction happens AFTER the game started up
|
||||||
|
if (count > 0 && count === audioContextList.length) {
|
||||||
|
userInputEventNames.forEach((eventName) => {
|
||||||
|
document.removeEventListener(eventName, resumeAllContexts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We bind the resume function for each user interaction
|
||||||
|
// event on the page
|
||||||
|
userInputEventNames.forEach((eventName) => {
|
||||||
|
document.addEventListener(eventName, resumeAllContexts);
|
||||||
|
});
|
||||||
|
})();
|
22
html/styles.css
Normal file
22
html/styles.css
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: lightgray;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bevy {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
14
index.html
Normal file
14
index.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" />
|
||||||
|
<title>game</title>
|
||||||
|
<link data-trunk rel="copy-dir" href="assets" />
|
||||||
|
<link rel="icon" href="icon.ico" />
|
||||||
|
<link data-trunk rel="inline" href="html/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<link data-trunk rel="inline" href="html/sound.js" />
|
||||||
|
</body>
|
||||||
|
</html>
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
36
src/lib.rs
Normal file
36
src/lib.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Bevy requires the use of quite complex types in queries,
|
||||||
|
// so we tell clippy to not mention this.
|
||||||
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use player::PlayerPlugin;
|
||||||
|
|
||||||
|
mod loading;
|
||||||
|
mod menu;
|
||||||
|
mod player;
|
||||||
|
|
||||||
|
use crate::{loading::LoadingPlugin, menu::MenuPlugin};
|
||||||
|
|
||||||
|
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
|
||||||
|
enum GameState {
|
||||||
|
#[default]
|
||||||
|
Loading,
|
||||||
|
Playing,
|
||||||
|
Menu,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct YunoPlugin;
|
||||||
|
|
||||||
|
impl Plugin for YunoPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_state::<GameState>()
|
||||||
|
.add_plugins((LoadingPlugin, MenuPlugin, PlayerPlugin));
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
app.add_plugins((FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin::default()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
src/loading.rs
Normal file
40
src/loading.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_asset_loader::{
|
||||||
|
asset_collection::AssetCollection,
|
||||||
|
loading_state::{config::ConfigureLoadingState, LoadingState, LoadingStateAppExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::GameState;
|
||||||
|
|
||||||
|
pub struct LoadingPlugin;
|
||||||
|
|
||||||
|
impl Plugin for LoadingPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_loading_state(
|
||||||
|
LoadingState::new(GameState::Loading)
|
||||||
|
.continue_to_state(GameState::Menu)
|
||||||
|
.load_collection::<TextureAssets>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AssetCollection, Resource)]
|
||||||
|
pub struct TextureAssets {
|
||||||
|
#[asset(texture_atlas_layout(
|
||||||
|
tile_size_x = 64,
|
||||||
|
tile_size_y = 64,
|
||||||
|
columns = 4,
|
||||||
|
rows = 4,
|
||||||
|
padding_x = 0,
|
||||||
|
padding_y = 0,
|
||||||
|
offset_x = 0,
|
||||||
|
offset_y = 0
|
||||||
|
))]
|
||||||
|
pub witch_layout: Handle<TextureAtlasLayout>,
|
||||||
|
#[asset(path = "textures/witch.png")]
|
||||||
|
pub witch: Handle<Image>,
|
||||||
|
#[asset(path = "textures/bevy.png")]
|
||||||
|
pub bevy: Handle<Image>,
|
||||||
|
#[asset(path = "textures/forgejo.png")]
|
||||||
|
pub forgejo: Handle<Image>,
|
||||||
|
}
|
54
src/main.rs
Normal file
54
src/main.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use bevy::{asset::AssetMetaCheck, prelude::*, window::PrimaryWindow, winit::WinitWindows};
|
||||||
|
use winit::window::Icon;
|
||||||
|
use yuno::YunoPlugin;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.insert_resource(Msaa::Off)
|
||||||
|
.insert_resource(ClearColor(Color::srgb(
|
||||||
|
17.0 / 255.0,
|
||||||
|
17.0 / 255.0,
|
||||||
|
27.0 / 255.0,
|
||||||
|
)))
|
||||||
|
.add_plugins(
|
||||||
|
DefaultPlugins
|
||||||
|
.set(ImagePlugin::default_nearest())
|
||||||
|
.set(WindowPlugin {
|
||||||
|
primary_window: Some(Window {
|
||||||
|
title: "yuno".to_string(),
|
||||||
|
canvas: Some("#bevy".to_owned()),
|
||||||
|
fit_canvas_to_parent: true,
|
||||||
|
prevent_default_event_handling: false,
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.set(AssetPlugin {
|
||||||
|
meta_check: AssetMetaCheck::Never,
|
||||||
|
..default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.add_plugins(YunoPlugin)
|
||||||
|
.add_systems(Startup, set_window_icon)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_window_icon(
|
||||||
|
windows: NonSend<WinitWindows>,
|
||||||
|
primary_window: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
) {
|
||||||
|
let primary_entity = primary_window.single();
|
||||||
|
let Some(primary) = windows.get_window(primary_entity) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let icon_buf = Cursor::new(include_bytes!("../assets/textures/icon.png"));
|
||||||
|
if let Ok(image) = image::load(icon_buf, image::ImageFormat::Png) {
|
||||||
|
let image = image.into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
let icon = Icon::from_rgba(rgba, width, height).expect("window icon");
|
||||||
|
primary.set_window_icon(Some(icon));
|
||||||
|
}
|
||||||
|
}
|
220
src/menu.rs
Normal file
220
src/menu.rs
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{loading::TextureAssets, GameState};
|
||||||
|
|
||||||
|
pub struct MenuPlugin;
|
||||||
|
|
||||||
|
impl Plugin for MenuPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(OnEnter(GameState::Menu), setup)
|
||||||
|
.add_systems(Update, click_button.run_if(in_state(GameState::Menu)))
|
||||||
|
.add_systems(OnExit(GameState::Menu), cleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct ButtonColours {
|
||||||
|
normal: Color,
|
||||||
|
hovered: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ButtonColours {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
normal: Color::srgb(88.0 / 255.0, 91.0 / 255.0, 112.0 / 255.0),
|
||||||
|
hovered: Color::srgb(116.0 / 255.0, 199.0 / 255.0, 236.0 / 255.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct Menu;
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands, textures: Res<TextureAssets>) {
|
||||||
|
commands.spawn((Camera2dBundle::default(), Menu));
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Menu,
|
||||||
|
))
|
||||||
|
.with_children(|children| {
|
||||||
|
let button_colours = ButtonColours::default();
|
||||||
|
children
|
||||||
|
.spawn((
|
||||||
|
ButtonBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(140.0),
|
||||||
|
height: Val::Px(90.0),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
background_color: button_colours.normal.into(),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
button_colours,
|
||||||
|
ChangeState(GameState::Playing),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn(TextBundle::from_section(
|
||||||
|
"Play",
|
||||||
|
TextStyle {
|
||||||
|
font_size: 40.0,
|
||||||
|
color: Color::srgb(205.0 / 255.0, 214.0 / 255.0, 244.0 / 255.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
flex_direction: FlexDirection::Row,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::SpaceAround,
|
||||||
|
bottom: Val::Px(5.),
|
||||||
|
width: Val::Percent(100.),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Menu,
|
||||||
|
))
|
||||||
|
.with_children(|children| {
|
||||||
|
children
|
||||||
|
.spawn((
|
||||||
|
ButtonBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(170.0),
|
||||||
|
height: Val::Px(50.0),
|
||||||
|
justify_content: JustifyContent::SpaceAround,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
padding: UiRect::all(Val::Px(5.)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
background_color: Color::NONE.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
ButtonColours {
|
||||||
|
normal: Color::NONE,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
OpenLink("https://bevyengine.org"),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn(TextBundle::from_section(
|
||||||
|
"Made with Bevy",
|
||||||
|
TextStyle {
|
||||||
|
font_size: 15.0,
|
||||||
|
color: Color::srgb(205.0 / 255.0, 214.0 / 255.0, 244.0 / 255.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
parent.spawn(ImageBundle {
|
||||||
|
image: textures.bevy.clone().into(),
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(32.),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
children
|
||||||
|
.spawn((
|
||||||
|
ButtonBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(170.0),
|
||||||
|
height: Val::Px(50.0),
|
||||||
|
justify_content: JustifyContent::SpaceAround,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
padding: UiRect::all(Val::Px(5.)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
background_color: Color::NONE.into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
ButtonColours {
|
||||||
|
normal: Color::NONE,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
OpenLink("https://git.ragarock.moe/silvana/yuno"),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn(TextBundle::from_section(
|
||||||
|
"repository",
|
||||||
|
TextStyle {
|
||||||
|
font_size: 15.0,
|
||||||
|
color: Color::srgb(205.0 / 255.0, 214.0 / 255.0, 244.0 / 255.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
parent.spawn(ImageBundle {
|
||||||
|
image: textures.forgejo.clone().into(),
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(32.),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct ChangeState(GameState);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct OpenLink(&'static str);
|
||||||
|
|
||||||
|
fn click_button(
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
mut interaction_query: Query<
|
||||||
|
(
|
||||||
|
&Interaction,
|
||||||
|
&mut BackgroundColor,
|
||||||
|
&ButtonColours,
|
||||||
|
Option<&ChangeState>,
|
||||||
|
Option<&OpenLink>,
|
||||||
|
),
|
||||||
|
(Changed<Interaction>, With<Button>),
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (interaction, mut colour, button_colours, change_state, open_link) in &mut interaction_query
|
||||||
|
{
|
||||||
|
match *interaction {
|
||||||
|
Interaction::Pressed => {
|
||||||
|
if let Some(state) = change_state {
|
||||||
|
next_state.set(state.0.clone());
|
||||||
|
} else if let Some(link) = open_link {
|
||||||
|
if let Err(error) = webbrowser::open(link.0) {
|
||||||
|
warn!("failed to open link {error:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Interaction::Hovered => {
|
||||||
|
*colour = button_colours.hovered.into();
|
||||||
|
}
|
||||||
|
Interaction::None => {
|
||||||
|
*colour = button_colours.normal.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(mut commands: Commands, menu: Query<Entity, With<Menu>>) {
|
||||||
|
for entity in menu.iter() {
|
||||||
|
commands.entity(entity).despawn_recursive();
|
||||||
|
}
|
||||||
|
}
|
95
src/player.rs
Normal file
95
src/player.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::{loading::TextureAssets, GameState};
|
||||||
|
|
||||||
|
pub struct PlayerPlugin;
|
||||||
|
|
||||||
|
const SPEED: f32 = 64.0;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Player;
|
||||||
|
|
||||||
|
impl Plugin for PlayerPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(OnEnter(GameState::Playing), spawn)
|
||||||
|
.add_systems(
|
||||||
|
Update,
|
||||||
|
(animate, movement).run_if(in_state(GameState::Playing)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct AnimationConfig {
|
||||||
|
first_sprite_index: usize,
|
||||||
|
last_sprite_index: usize,
|
||||||
|
frame_timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnimationConfig {
|
||||||
|
fn new(first_sprite_index: usize, last_sprite_index: usize, fps: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
first_sprite_index,
|
||||||
|
last_sprite_index,
|
||||||
|
frame_timer: Self::timer_from_fps(fps),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timer_from_fps(fps: u8) -> Timer {
|
||||||
|
Timer::new(
|
||||||
|
Duration::from_secs_f32(1.0 / fps as f32),
|
||||||
|
TimerMode::Repeating,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn(mut commands: Commands, textures: Res<TextureAssets>) {
|
||||||
|
let mut camera = Camera2dBundle::default();
|
||||||
|
camera.projection.scale *= 0.25;
|
||||||
|
commands.spawn(camera);
|
||||||
|
|
||||||
|
let animation_config = AnimationConfig::new(0, 3, 10);
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
SpriteBundle {
|
||||||
|
texture: textures.witch.clone(),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextureAtlas::from(textures.witch_layout.clone()),
|
||||||
|
animation_config,
|
||||||
|
Player,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animate(time: Res<Time>, mut query: Query<(&mut AnimationConfig, &mut TextureAtlas)>) {
|
||||||
|
for (mut config, mut atlas) in &mut query {
|
||||||
|
config.frame_timer.tick(time.delta());
|
||||||
|
|
||||||
|
if config.frame_timer.just_finished() {
|
||||||
|
atlas.index = if atlas.index == config.last_sprite_index {
|
||||||
|
config.first_sprite_index
|
||||||
|
} else {
|
||||||
|
atlas.index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn movement(
|
||||||
|
time: Res<Time>,
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut player_query: Query<&mut Transform, With<Player>>,
|
||||||
|
) {
|
||||||
|
let mut transform = player_query.single_mut();
|
||||||
|
|
||||||
|
let direction = Vec3::new(
|
||||||
|
keys.pressed(KeyCode::KeyD) as i8 as f32 - keys.pressed(KeyCode::KeyA) as i8 as f32,
|
||||||
|
keys.pressed(KeyCode::KeyW) as i8 as f32 - keys.pressed(KeyCode::KeyS) as i8 as f32,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let velocity = direction.normalize_or_zero();
|
||||||
|
transform.translation += velocity * time.delta_seconds() * SPEED;
|
||||||
|
}
|
Loading…
Reference in a new issue