Add(yuno): base project.
All checks were successful
ci/woodpecker/pr/fmt Pipeline was successful
ci/woodpecker/pr/clippy Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

This commit is contained in:
silvana 2024-08-04 09:28:54 +02:00
commit ff9950a89d
Signed by: silvana
GPG key ID: 889CA3FB0F54CDCC
22 changed files with 5228 additions and 0 deletions

6
.cargo/config.toml Normal file
View 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
View file

@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/dist

11
.woodpecker/clippy.yml Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

73
Cargo.toml Normal file
View 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
View 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

Binary file not shown.

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

Binary file not shown.

BIN
assets/textures/witch.png (Stored with Git LFS) Normal file

Binary file not shown.

62
html/sound.js Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

36
src/lib.rs Normal file
View 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
View 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
View 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
View 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
View 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;
}