LD45/src/main.rs

1792 lines
62 KiB
Rust

use quicksilver::{
geom::{Circle, Rectangle, Transform, Vector},
graphics::{
Background::{Blended, Col, Img},
Color, Font, FontStyle, Image, View,
},
input::{ButtonState, Key},
lifecycle::{run, Asset, Event, Settings, State, Window},
saving::{load, save},
sound::Sound,
Result,
};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
const WIDTH_F: f32 = 800.0;
const HEIGHT_F: f32 = 600.0;
const MUSIC2_LENGTH: f64 = 2.0 * 60.0 * 1000.0;
const TEXT_RATE: f64 = 100.0;
const PP_GEN_RATE: f64 = 75.0;
const PARTICLE_RAND_VEL_RANGE: f32 = 0.2;
const PARTICLE_RAND_VEL_DIST: f32 = 0.2828427; // dist where x and y = 0.2
const PARTICLE_RAND_ROT_RANGE: f32 = 0.5;
const JOINING_OPACITY_RATE: f32 = 0.00013;
const JOINING_FAR_DIST: f32 = 700.0;
const JOINING_NEAR_DIST: f32 = 150.0;
const DOUBLE_CLICK_TIME: f64 = 350.0;
const SL_NOTIF_TIME: f64 = 5000.0;
const MAX_MOONS: usize = 5;
fn interp_sq_inv(x: f32) -> f32 {
if x < 0.0 {
return 0.0;
} else if x > 1.0 {
return 1.0;
}
let y = x - 1.0;
-y * y + 1.0
}
fn interp_sq(x: f32) -> f32 {
if x < 0.0 {
return 0.0;
} else if x > 1.0 {
return 1.0;
}
x * x
}
enum MenuItemType {
Button {
text: &'static str,
text_image: Option<Image>,
text_c: Color,
h_c: Color,
c: Color,
},
AppearingText {
text: &'static str,
text_image: Option<Image>,
current_text: String,
text_size: f32,
text_c: Color,
timer: f64,
},
InstantText {
text: &'static str,
text_image: Option<Image>,
text_size: f32,
text_color: Color,
},
Pause {
timer: f64,
length: f64,
},
}
struct MenuItem {
x: f32,
y: f32,
w: f32,
h: f32,
item_type: MenuItemType,
is_hover: bool,
is_focus: bool,
is_loaded: bool,
}
impl MenuItem {
fn is_inside(&self, x: f32, y: f32) -> bool {
x >= self.x && x < self.x + self.w && y >= self.y && y < self.y + self.h
}
}
struct Menu {
items: Vec<MenuItem>,
}
impl Menu {
fn button(
x: f32,
y: f32,
w: f32,
h: f32,
s: &'static str,
t_color: Color,
box_color: Color,
boxh_color: Color,
first: bool,
) -> MenuItem {
MenuItem {
x,
y,
w,
h,
item_type: MenuItemType::Button {
text: s,
text_image: None,
text_c: t_color,
h_c: boxh_color,
c: box_color,
},
is_hover: false,
is_focus: false,
is_loaded: !first,
}
}
fn start() -> Menu {
let item = MenuItem {
x: WIDTH_F / 2.0 - 120.0,
y: 150.0,
w: 240.0,
h: 150.0,
item_type: MenuItemType::Button {
text: "Start the Game",
text_image: None,
text_c: Color::WHITE,
h_c: Color::from_rgba(0x66, 0xFF, 0xFF, 1.0),
c: Color::from_rgba(0x33, 0xDD, 0xDD, 1.0),
},
is_hover: false,
is_focus: false,
is_loaded: false,
};
Menu {
items: vec![
item,
Menu::instant_text(
70.0,
50.0,
55.0,
true,
"One And All - A Ludum Dare 45 Entry",
),
Menu::instant_text(
25.0,
HEIGHT_F - 100.0,
30.0,
true,
"Made with quicksilver which is licensed with either",
),
Menu::instant_text(
70.0,
HEIGHT_F - 80.0,
30.0,
true,
"MIT License or Apache License Version 2.0",
),
Menu::instant_text(
25.0,
HEIGHT_F - 50.0,
30.0,
true,
"Uses Clear-Sans which is licensed with Apache License Version 2.0",
),
],
}
}
fn instant_text(x: f32, y: f32, text_size: f32, first: bool, s: &'static str) -> MenuItem {
MenuItem {
x,
y,
w: 0.0,
h: 0.0,
item_type: MenuItemType::InstantText {
text: s,
text_image: None,
text_size,
text_color: Color::WHITE,
},
is_hover: false,
is_focus: false,
is_loaded: !first,
}
}
fn text(x: f32, y: f32, text_size: f32, first: bool, s: &'static str) -> MenuItem {
MenuItem {
x,
y,
w: 0.0,
h: 0.0,
item_type: MenuItemType::AppearingText {
text: s,
text_image: None,
text_size,
current_text: String::new(),
text_c: Color::WHITE,
timer: 0.0,
},
is_hover: false,
is_focus: false,
is_loaded: !first,
}
}
fn pause(length: f64, first: bool) -> MenuItem {
MenuItem {
x: 0.0,
y: 0.0,
w: 0.0,
h: 0.0,
item_type: MenuItemType::Pause { timer: 0.0, length },
is_hover: false,
is_focus: false,
is_loaded: !first,
}
}
fn s_01() -> Menu {
Menu {
items: vec![
Menu::pause(500.0, true),
Menu::text(50.0, HEIGHT_F - 140.0, 40.0, false, "This is how it is."),
Menu::pause(500.0, false),
Menu::text(
50.0,
HEIGHT_F - 100.0,
40.0,
false,
"Nothing is, and everything is nothing.",
),
Menu::pause(500.0, false),
Menu::text(50.0, HEIGHT_F - 60.0, 40.0, false, "...until you appeared."),
Menu::pause(100.0, false),
Menu::text(
570.0,
HEIGHT_F - 50.0,
30.0,
false,
"(Click to continue...)",
),
],
}
}
fn s_02() -> Menu {
Menu {
items: vec![
Menu::text(
50.0,
HEIGHT_F - 150.0,
40.0,
true,
"Just by being, you brought light into existence.",
),
Menu::pause(500.0, false),
Menu::text(
50.0,
HEIGHT_F - 110.0,
40.0,
false,
"What brings you here? What drives you?",
),
Menu::pause(500.0, false),
Menu::text(
50.0,
HEIGHT_F - 70.0,
40.0,
false,
"Please tell me, what fuels you?",
),
Menu::button(
100.0,
30.0,
200.0,
85.0,
"Hope",
Color::WHITE,
Color::BLACK,
Color::from_rgba(0x33, 0x33, 0x33, 1.0),
false,
),
Menu::button(
500.0,
30.0,
200.0,
85.0,
"Miracles",
Color::WHITE,
Color::BLACK,
Color::from_rgba(0x33, 0x33, 0x33, 1.0),
false,
),
Menu::button(
100.0,
150.0,
200.0,
85.0,
"Kindness",
Color::WHITE,
Color::BLACK,
Color::from_rgba(0x33, 0x33, 0x33, 1.0),
false,
),
Menu::button(
500.0,
150.0,
200.0,
85.0,
"Determination",
Color::WHITE,
Color::BLACK,
Color::from_rgba(0x33, 0x33, 0x33, 1.0),
false,
),
],
}
}
// choose hope
fn s_03() -> Menu {
Menu {
items: vec![
Menu::text(
50.0,
HEIGHT_F - 170.0,
40.0,
true,
"Hope... hope that your actions will inspire others..",
),
Menu::pause(500.0, false),
Menu::text(
50.0,
HEIGHT_F - 130.0,
40.0,
false,
"Hope that a brighter future will come tomorrow...",
),
Menu::pause(500.0, false),
Menu::text(
50.0,
HEIGHT_F - 90.0,
40.0,
false,
".. With your appearance, perhaps it shall...",
),
],
}
}
// choose miracles
fn s_04() -> Menu {
Menu {
items: vec![
Menu::text(
30.0,
HEIGHT_F - 170.0,
40.0,
true,
"Miracles huh?.. I see, then your appearance is special.",
),
Menu::pause(500.0, false),
Menu::text(
30.0,
HEIGHT_F - 130.0,
40.0,
false,
"With your appearance, things may change for the better..",
),
Menu::pause(500.0, false),
Menu::text(
30.0,
HEIGHT_F - 90.0,
40.0,
false,
"Now I am certain that this meeting was not by chance.",
),
],
}
}
// choose kindness
fn s_05() -> Menu {
Menu {
items: vec![
Menu::text(
50.0,
HEIGHT_F - 170.0,
40.0,
true,
"Kindness?.. I am in your debt.",
),
Menu::pause(250.0, false),
Menu::text(
50.0,
HEIGHT_F - 130.0,
40.0,
false,
"It has been a long time since I have encountered",
),
Menu::text(50.0, HEIGHT_F - 90.0, 40.0, false, "another being..."),
Menu::pause(500.0, false),
Menu::text(270.0, HEIGHT_F - 90.0, 40.0, false, "... Thank you..."),
],
}
}
// choose determination
fn s_06() -> Menu {
Menu {
items: vec![
Menu::text(
50.0,
HEIGHT_F - 170.0,
40.0,
true,
"Determination.. I see...",
),
Menu::pause(500.0, false),
Menu::text(
400.0,
HEIGHT_F - 170.0,
40.0,
false,
"I do not doubt it, for it",
),
Menu::text(
50.0,
HEIGHT_F - 130.0,
40.0,
false,
"must have been difficult to come here..",
),
Menu::pause(500.0, false),
Menu::text(
50.0,
HEIGHT_F - 90.0,
40.0,
false,
"Your resolve is evident by your mere presence..",
),
],
}
}
fn s_07() -> Menu {
Menu {
items: vec![
Menu::text(
50.0,
HEIGHT_F - 130.0,
40.0,
true,
"Now that you are here, it must mean a new era of",
),
Menu::text(
50.0,
HEIGHT_F - 90.0,
40.0,
false,
"creation for all that will be.",
),
Menu::pause(200.0, false),
Menu::text(
50.0,
HEIGHT_F - 50.0,
40.0,
false,
"Try double-clicking the void to create something...",
),
],
}
}
fn s_08() -> Menu {
Menu {
items: vec![Menu::instant_text(
50.0,
HEIGHT_F - 90.0,
35.0,
true,
"(Try double-clicking now...)",
)],
}
}
fn s_09() -> Menu {
Menu {
items: vec![
Menu::pause(400.0, true),
Menu::text(
50.0,
HEIGHT_F - 140.0,
40.0,
false,
"A new planet... It has most certainly been a while.",
),
Menu::pause(500.0, false),
Menu::text(
50.0,
HEIGHT_F - 100.0,
40.0,
false,
"Please, go out and create the new universe, and again..",
),
Menu::pause(300.0, false),
Menu::text(50.0, HEIGHT_F - 60.0, 40.0, false, "Thank you."),
],
}
}
fn s_10() -> Menu {
Menu {
items: vec![
Menu::instant_text(
20.0,
HEIGHT_F - 40.0,
20.0,
true,
"Single click to move, Double-click to create something",
),
Menu::instant_text(
20.0,
HEIGHT_F - 20.0,
20.0,
true,
"S - save; L - load (can load from the start); R - reset",
),
],
}
}
}
#[derive(Serialize, Deserialize, Clone)]
struct Particle {
rect: Rectangle,
circle: Circle,
is_rect: bool,
velx: f32,
vely: f32,
velr: f32,
r: f32,
lifetime: f64,
life_timer: f64,
}
#[derive(Serialize, Deserialize, Clone)]
struct ParticleSystem {
particles: Vec<Particle>,
spawn_timer: f64,
spawn_time: f64,
lifetime: f64,
host_rect: Rectangle,
host_circle: Circle,
is_rect: bool,
direction: Vector,
color: Color,
opacity: f32,
vel_multiplier: f32,
}
impl ParticleSystem {
fn new(
spawn_time: f64,
lifetime: f64,
host_rect: Rectangle,
host_circle: Circle,
is_rect: bool,
direction: Vector,
color: Color,
opacity: f32,
vel_multiplier: f32,
) -> Self {
Self {
particles: Vec::new(),
spawn_timer: 0.0,
spawn_time,
lifetime,
host_rect,
host_circle,
is_rect,
direction,
color,
opacity,
vel_multiplier,
}
}
fn update(&mut self, dt: f64) {
for i in (0..self.particles.len()).rev() {
self.particles[i].life_timer += dt;
if self.particles[i].life_timer > self.particles[i].lifetime {
self.particles.swap_remove(i);
} else {
if self.is_rect {
self.particles[i].rect.pos.x += self.particles[i].velx * dt as f32;
self.particles[i].rect.pos.y += self.particles[i].vely * dt as f32;
self.particles[i].r += self.particles[i].velr * dt as f32;
} else {
self.particles[i].circle.pos.x += self.particles[i].velx * dt as f32;
self.particles[i].circle.pos.y += self.particles[i].vely * dt as f32;
}
}
}
self.spawn_timer += dt;
if self.spawn_timer > self.spawn_time {
self.spawn_timer -= self.spawn_time;
self.particles.push(Particle {
rect: self.host_rect,
circle: self.host_circle,
is_rect: self.is_rect,
velx: (rand::thread_rng()
.gen_range(-PARTICLE_RAND_VEL_RANGE, PARTICLE_RAND_VEL_RANGE)
+ self.direction.x)
* self.vel_multiplier,
vely: (rand::thread_rng()
.gen_range(-PARTICLE_RAND_VEL_RANGE, PARTICLE_RAND_VEL_RANGE)
+ self.direction.y)
* self.vel_multiplier,
// velx: self.direction.x,
// vely: self.direction.y,
velr: rand::thread_rng()
.gen_range(-PARTICLE_RAND_ROT_RANGE, PARTICLE_RAND_ROT_RANGE)
* self.vel_multiplier,
r: rand::thread_rng().gen_range(0.0, 90.0),
lifetime: self.lifetime,
life_timer: 0.0,
});
}
}
fn draw(&mut self, window: &mut Window, transform: Transform) {
if self.opacity == 0.0 {
return;
}
for particle in &mut self.particles {
self.color.a = (1.0 - (particle.life_timer / particle.lifetime) as f32) * self.opacity;
if particle.is_rect {
let pre_transform = Transform::translate((
-particle.rect.size.x / 2.0,
-particle.rect.size.y / 2.0,
)) * Transform::rotate(particle.r);
window.draw_ex(
&particle.rect,
Col(self.color),
transform * pre_transform,
1,
);
} else {
window.draw_ex(&particle.circle, Col(self.color), transform, 1);
}
}
}
fn force_spawn(&mut self, count: usize) {
for i in 0..count {
self.particles.push(Particle {
rect: self.host_rect,
circle: self.host_circle,
is_rect: self.is_rect,
velx: (rand::thread_rng()
.gen_range(-PARTICLE_RAND_VEL_RANGE, PARTICLE_RAND_VEL_RANGE)
+ self.direction.x)
* self.vel_multiplier,
vely: (rand::thread_rng()
.gen_range(-PARTICLE_RAND_VEL_RANGE, PARTICLE_RAND_VEL_RANGE)
+ self.direction.y)
* self.vel_multiplier,
// velx: self.direction.x,
// vely: self.direction.y,
velr: rand::thread_rng()
.gen_range(-PARTICLE_RAND_ROT_RANGE, PARTICLE_RAND_ROT_RANGE)
* self.vel_multiplier,
r: rand::thread_rng().gen_range(0.0, 90.0),
lifetime: self.lifetime,
life_timer: 0.0,
});
}
}
}
#[derive(Serialize, Deserialize, Clone)]
struct RotatingParticleSystem {
particle_system: ParticleSystem,
r: f32,
velr: f32,
offset: f32,
}
impl RotatingParticleSystem {
fn new(
spawn_time: f64,
lifetime: f64,
host_rect: Rectangle,
host_circle: Circle,
is_rect: bool,
direction: Vector,
color: Color,
opacity: f32,
rotation: f32,
velr: f32,
offset: f32,
vel_multiplier: f32,
) -> Self {
RotatingParticleSystem {
particle_system: ParticleSystem::new(
spawn_time,
lifetime,
host_rect,
host_circle,
is_rect,
direction,
color,
opacity,
vel_multiplier,
),
r: rotation,
velr,
offset,
}
}
fn update(&mut self, dt: f64) {
if self.particle_system.is_rect {
let saved_rect = self.particle_system.host_rect;
self.particle_system.host_rect.pos +=
Transform::rotate(self.r) * Vector::new(self.offset, 0.0);
self.particle_system.update(dt);
self.particle_system.host_rect = saved_rect;
} else {
let saved_cir = self.particle_system.host_circle;
self.particle_system.host_circle.pos +=
Transform::rotate(self.r) * Vector::new(self.offset, 0.0);
self.particle_system.update(dt);
self.particle_system.host_circle = saved_cir;
}
self.r += self.velr * dt as f32;
}
fn draw(&mut self, window: &mut Window, transform: Transform) {
if self.particle_system.opacity == 0.0 {
return;
}
self.particle_system.direction =
Transform::rotate(self.r) * Vector::new(0.0, -PARTICLE_RAND_VEL_DIST);
self.particle_system.draw(window, transform);
if self.particle_system.is_rect {
let mut moved_rect = self.particle_system.host_rect;
moved_rect.pos += Transform::rotate(self.r) * Vector::new(self.offset, 0.0);
let mut solid_color = self.particle_system.color;
solid_color.a = self.particle_system.opacity;
window.draw_ex(
&moved_rect,
Col(solid_color),
transform
* Transform::translate((-moved_rect.size.x / 2.0, -moved_rect.size.y / 2.0))
* Transform::rotate(self.r * 1.3),
1,
);
} else {
let mut moved_cir = self.particle_system.host_circle;
moved_cir.pos += Transform::rotate(self.r) * Vector::new(self.offset, 0.0);
let mut solid_color = self.particle_system.color;
solid_color.a = self.particle_system.opacity;
window.draw_ex(&moved_cir, Col(solid_color), transform, 1);
}
}
}
struct ExplConvCircleParticle {
circle: Circle,
offset: f32,
r: f32,
}
struct ExplConvParticleSystem {
particles: Vec<ExplConvCircleParticle>,
lifetime: f64,
host_circle: Circle,
color: Color,
opacity: f32,
life_timer: f64,
}
impl ExplConvParticleSystem {
fn new(lifetime: f64, host_circle: Circle, color: Color, opacity: f32) -> Self {
ExplConvParticleSystem {
particles: Vec::new(),
lifetime,
host_circle,
color,
opacity,
life_timer: 0.0,
}
}
fn activate(&mut self, count: usize, offset: f32) {
self.life_timer = 0.0;
for i in 0..count {
self.particles.push(ExplConvCircleParticle {
circle: self.host_circle,
offset,
r: rand::thread_rng().gen_range(0.0, 360.0),
});
}
}
// returns true if finished
fn update(&mut self, dt: f64, planets: &mut Vec<Planet>) -> bool {
self.life_timer += dt;
if self.life_timer >= self.lifetime {
if !self.particles.is_empty() {
self.particles.clear();
planets.push(Planet::new(self.host_circle, self.color));
return true;
}
return false;
}
if self.life_timer < self.lifetime / 2.0 {
let amount = interp_sq_inv((self.life_timer / self.lifetime) as f32 * 2.0);
for particle in &mut self.particles {
let dir =
Transform::rotate(particle.r) * Vector::new(particle.offset * amount, 0.0);
particle.circle.pos = dir + self.host_circle.pos;
}
} else {
let amount = 1.0 - interp_sq(((self.life_timer / self.lifetime) as f32 - 0.5) * 2.0);
for particle in &mut self.particles {
let dir =
Transform::rotate(particle.r) * Vector::new(particle.offset * amount, 0.0);
particle.circle.pos = dir + self.host_circle.pos;
}
}
return false;
}
fn draw(&mut self, window: &mut Window, transform: Transform) {
if self.opacity == 0.0 {
return;
}
for particle in &mut self.particles {
self.color.a = ((self.life_timer / self.lifetime) as f32 / 2.0 + 0.5) * self.opacity;
window.draw_ex(&particle.circle, Col(self.color), transform, 1);
}
}
}
#[derive(Serialize, Deserialize, Clone)]
struct Planet {
circle: Circle,
color: Color,
particle_system: ParticleSystem,
moons: Vec<RotatingParticleSystem>,
}
impl Planet {
fn new(circle: Circle, color: Color) -> Self {
let mut smaller_circle = circle;
smaller_circle.radius /= 4.0;
let mut planet = Planet {
circle,
color,
particle_system: ParticleSystem::new(
rand::thread_rng().gen_range(2000.0, 3800.0),
900.0,
Rectangle::new((0.0, 0.0), (1.0, 1.0)),
circle,
false,
Vector::new(0.0, 0.0),
color,
1.0,
0.3,
),
moons: Vec::with_capacity(MAX_MOONS),
};
let r: f32 = rand::thread_rng().gen_range(0.0, 360.0);
let clockwise = rand::thread_rng().gen_bool(0.5);
for i in 0..rand::thread_rng().gen_range(0, MAX_MOONS) {
planet.moons.push(RotatingParticleSystem::new(
rand::thread_rng().gen_range(1000.0, 2600.0),
600.0,
Rectangle::new((0.0, 0.0), (1.0, 1.0)),
smaller_circle,
false,
Vector::new(0.0, 0.0),
color,
1.0,
r,
if clockwise {
rand::thread_rng().gen_range(0.05, 0.15)
} else {
rand::thread_rng().gen_range(-0.15, -0.05)
},
rand::thread_rng().gen_range(35.0, 200.0),
0.2,
));
}
planet
}
fn update(&mut self, dt: f64) {
self.particle_system.host_circle.pos = self.circle.pos;
self.particle_system.update(dt);
for moon in &mut self.moons {
moon.particle_system.host_circle.pos = self.circle.pos;
moon.update(dt);
}
}
fn draw(&mut self, window: &mut Window, transform: Transform) {
self.particle_system.draw(window, transform);
window.draw_ex(&self.circle, Col(self.color), transform, 1);
for moon in &mut self.moons {
moon.draw(window, transform);
}
}
}
#[derive(Clone, Serialize, Deserialize)]
struct Star {
color: Color,
particle_system: ParticleSystem,
velr: f32,
r: f32,
}
impl Star {
fn new(circle: Circle, color: Color, velr: f32, r: f32) -> Self {
let mut star = Star {
color,
particle_system: ParticleSystem::new(
rand::thread_rng().gen_range(80.0, 200.0),
850.0,
Rectangle::new((0.0, 0.0), (1.0, 1.0)),
circle,
false,
Vector::new(0.0, 0.0),
color,
1.0,
1.0,
),
velr,
r,
};
if star.color.r < 0.75 {
star.color.r = 0.75;
}
if star.color.g < 0.75 {
star.color.g = 0.75;
}
if star.color.b < 0.75 {
star.color.b = 0.75;
}
star.particle_system
.force_spawn(rand::thread_rng().gen_range(20, 45));
star
}
fn update(&mut self, dt: f64) {
self.particle_system.update(dt);
self.r += self.velr * dt as f32;
}
fn draw(&mut self, image: &mut Image, window: &mut Window, transform: Transform) {
self.particle_system.draw(window, transform);
let mut image_rect = image.area();
image_rect.pos = self.particle_system.host_circle.pos - image_rect.size / 2.0;
window.draw_ex(
&image_rect,
Blended(image, self.color),
transform * Transform::rotate(self.r),
1,
);
}
}
#[derive(Serialize, Deserialize, Clone)]
struct SaveData {
planets: Vec<Planet>,
stars: Vec<Star>,
player: Rectangle,
joining_particles: RotatingParticleSystem,
}
enum SaveLoadNotification {
Save { text: Option<Image>, timer: f64 },
Load { text: Option<Image>, timer: f64 },
}
struct GameState {
s_boom: Asset<Sound>,
s_get: Asset<Sound>,
s_power_up: Asset<Sound>,
s_tap: Asset<Sound>,
s_speak_m: Asset<Sound>,
s_speak_f: Asset<Sound>,
font: Asset<Font>,
music2: Asset<Sound>,
i_star: Option<Asset<Image>>,
i_star_actual: Option<Image>,
music_on: bool,
music_timer: f64,
menu: Menu,
state: u32,
state_dirty: bool,
selection_mode: bool,
current_item: Option<usize>,
current_finished: bool,
player: Rectangle,
player_r: f64,
player_particles: ParticleSystem,
joining_particles: RotatingParticleSystem,
is_create_mode: bool,
click_release_time: f64,
dbl_click_timeout: Option<f64>,
click_time: Option<f64>,
click_pos: Vector,
mouse_pos: Vector,
expl_conv_p_systems: Vec<ExplConvParticleSystem>,
planets: Vec<Planet>,
stars: Vec<Star>,
camera: Rectangle,
move_to: Vector,
save_load_notification: Option<SaveLoadNotification>,
}
impl State for GameState {
fn new() -> Result<Self> {
Ok(Self {
s_boom: Asset::new(Sound::load("boom.mp3")),
s_get: Asset::new(Sound::load("get.mp3")),
s_power_up: Asset::new(Sound::load("power_up.mp3")),
s_tap: Asset::new(Sound::load("tap.mp3")),
s_speak_m: Asset::new(Sound::load("speak_m.mp3")),
s_speak_f: Asset::new(Sound::load("speak_f.mp3")),
font: Asset::new(Font::load("ClearSans-Regular.ttf")),
music2: Asset::new(Sound::load("music2.mp3")),
i_star: Some(Asset::new(Image::load("star.png"))),
i_star_actual: None,
music_on: false,
music_timer: 0.0,
menu: Menu::start(),
state: 0,
state_dirty: false,
selection_mode: true,
current_item: None,
current_finished: true,
player: Rectangle::new((400.0, 300.0), (32.0, 32.0)),
player_r: 0.0,
player_particles: ParticleSystem::new(
PP_GEN_RATE,
1000.0,
Rectangle::new((400.0, 300.0), (32.0, 32.0)),
Circle::new((100.0, 100.0), 32.0),
true,
Vector::new(0.0, 0.0),
Color::WHITE,
0.0,
1.0,
),
joining_particles: RotatingParticleSystem::new(
PP_GEN_RATE,
1000.0,
Rectangle::new((400.0, 300.0), (16.0, 16.0)),
Circle::new((100.0, 100.0), 32.0),
true,
Vector::new(0.0, 0.0),
Color::GREEN,
0.0,
0.0,
0.1,
JOINING_FAR_DIST,
1.0,
),
is_create_mode: false,
click_release_time: 0.0,
dbl_click_timeout: None,
click_time: None,
click_pos: Vector::new(0.0, 0.0),
mouse_pos: Vector::new(0.0, 0.0),
expl_conv_p_systems: Vec::new(),
planets: Vec::new(),
stars: Vec::new(),
camera: Rectangle::new((0.0, 0.0), (WIDTH_F, HEIGHT_F)),
move_to: Vector::new(400.0, 300.0),
save_load_notification: None,
})
}
fn event(&mut self, event: &Event, window: &mut Window) -> Result<()> {
match event {
Event::MouseMoved(v) => {
self.mouse_pos = *v;
let mut hovered = false;
for i in 0..self.menu.items.len() {
if self.menu.items[i].is_inside(v.x, v.y) {
self.menu.items[i].is_hover = true;
self.current_item = Some(i);
hovered = true;
} else {
self.menu.items[i].is_hover = false;
}
}
if !hovered {
self.current_item = None;
}
}
Event::MouseButton(button, state) => {
if let ButtonState::Released = state {
if self.dbl_click_timeout.is_none() {
self.click_release_time = 0.0;
}
} else if let ButtonState::Pressed = state {
if self.current_finished {
if self.is_create_mode {
if self.click_release_time < DOUBLE_CLICK_TIME {
self.click_release_time = DOUBLE_CLICK_TIME;
self.dbl_click_timeout = Some(0.0);
self.click_time = None;
if self.state == 8 {
let mut expl_conv_system = ExplConvParticleSystem::new(
1500.0,
Circle::new(self.mouse_pos, 20.0),
Color::from_rgba(0x99, 0xFF, 0x99, 1.0),
1.0,
);
expl_conv_system.activate(30, 200.0);
self.expl_conv_p_systems.push(expl_conv_system);
self.state = 9;
self.state_dirty = true;
self.s_boom.execute(|s| {
s.set_volume(0.8);
s.play()
})?;
} else if self.state == 10 {
let mut rng = rand::thread_rng();
let rand_out = rng.gen_range(0.0, 1.0);
if rand_out < 0.75 {
// spawn planet
let mut expl_conv_system = ExplConvParticleSystem::new(
rng.gen_range(1200.0, 1600.0),
Circle::new(self.mouse_pos, rng.gen_range(15.0, 25.0)),
Color::from_rgba(
rng.gen_range(0x44, 0xFF),
rng.gen_range(0x44, 0xFF),
rng.gen_range(0x44, 0xFF),
1.0,
),
1.0,
);
expl_conv_system.activate(
rng.gen_range(13, 40),
rng.gen_range(150.0, 300.0),
);
self.expl_conv_p_systems.push(expl_conv_system);
} else {
// spawn star
let rot_clockwise = rng.gen_bool(0.5);
self.stars.push(Star::new(
Circle::new(self.mouse_pos, rng.gen_range(3.0, 7.0)),
Color::from_rgba(
rng.gen_range(0x58, 0xFF),
rng.gen_range(0x58, 0xFF),
rng.gen_range(0x58, 0xFF),
1.0,
),
if rot_clockwise {
rng.gen_range(0.1, 0.3)
} else {
rng.gen_range(-0.3, -0.1)
},
rng.gen_range(0.0, 90.0),
));
}
self.s_boom.execute(|s| {
s.set_volume(0.8);
s.play()
})?;
}
} else if self.state == 10 {
self.click_time = Some(0.0);
self.click_pos = self.mouse_pos;
}
} else if self.selection_mode {
if let Some(idx) = self.current_item {
match self.state {
0 => {
self.state += 1;
self.state_dirty = true;
}
2 => {
if idx == 5 {
// hope
self.state = 3;
self.state_dirty = true;
self.joining_particles.particle_system.color =
Color::from_rgba(0xAA, 0xCC, 0xFF, 1.0);
} else if idx == 6 {
// miracles
self.state = 4;
self.state_dirty = true;
self.joining_particles.particle_system.color =
Color::from_rgba(0xFF, 0xFF, 0xAA, 1.0);
} else if idx == 7 {
// kindness
self.state = 5;
self.state_dirty = true;
self.joining_particles.particle_system.color =
Color::from_rgba(0xBB, 0xFF, 0xBB, 1.0);
} else {
// determination
self.state = 6;
self.state_dirty = true;
self.joining_particles.particle_system.color =
Color::from_rgba(0xFF, 0xAA, 0xAA, 1.0);
}
self.s_get.execute(|s| {
s.set_volume(0.7);
s.play()
})?;
}
_ => {
self.state = 0;
self.state_dirty = true;
}
}
}
} else {
match self.state {
0 | 1 => self.state += 1,
3 | 4 | 5 | 6 => self.state = 7,
7 => self.state = 8,
9 => self.state = 10,
_ => self.state = 0,
}
self.state_dirty = true;
}
} else {
for mi in &mut self.menu.items {
match &mut mi.item_type {
MenuItemType::AppearingText {
text,
text_image,
current_text,
text_size,
text_c,
timer,
} => {
self.font.execute(|f| {
*current_text = text.to_string();
let style = FontStyle::new(*text_size, *text_c);
*text_image = Some(f.render(text, &style)?);
Ok(())
})?;
}
MenuItemType::Button {
text,
text_image,
text_c,
h_c,
c,
} => {
if text_image.is_none() {
self.font.execute(|font| {
let style = FontStyle::new(42.0, *text_c);
*text_image = Some(font.render(text, &style)?);
Ok(())
})?;
}
}
MenuItemType::Pause { timer, length } => (),
MenuItemType::InstantText {
text,
text_image,
text_size,
text_color,
} => {
if text_image.is_none() {
self.font.execute(|f| {
let style = FontStyle::new(*text_size, *text_color);
*text_image = Some(f.render(text, &style)?);
Ok(())
})?;
}
}
}
mi.is_loaded = true;
}
self.current_finished = true;
}
}
}
Event::Key(key, state) => {
if let ButtonState::Pressed = state {
match key {
Key::S => {
if self.state == 10 {
let save_data = SaveData {
planets: self.planets.clone(),
stars: self.stars.clone(),
player: self.player.clone(),
joining_particles: self.joining_particles.clone(),
};
save("OneAndAll_LD45", "slot0", &save_data)?;
self.save_load_notification = Some(SaveLoadNotification::Save {
text: None,
timer: SL_NOTIF_TIME,
});
}
}
Key::L => {
let load_result = load::<SaveData>("OneAndAll_LD45", "slot0");
if let Ok(save_data) = load_result {
self.planets = save_data.planets.clone();
self.stars = save_data.stars.clone();
self.player = save_data.player.clone();
self.joining_particles = save_data.joining_particles.clone();
self.expl_conv_p_systems.clear();
self.move_to = self.player.pos;
self.camera.pos =
self.player.pos - Vector::new(WIDTH_F / 2.0, HEIGHT_F / 2.0);
self.dbl_click_timeout = None;
self.click_time = None;
self.click_release_time = DOUBLE_CLICK_TIME;
self.state = 10;
self.state_dirty = true;
self.save_load_notification = Some(SaveLoadNotification::Load {
text: None,
timer: SL_NOTIF_TIME,
});
}
}
Key::R => {
if self.state == 10 {
self.state = 0;
self.state_dirty = true;
}
}
_ => (),
}
}
}
_ => (),
}
Ok(())
}
fn update(&mut self, window: &mut Window) -> Result<()> {
let dt = window.update_rate();
self.click_release_time += dt;
if let Some(t) = &mut self.click_time {
*t += dt;
if *t > DOUBLE_CLICK_TIME {
self.move_to = self.click_pos; // - Vector::new(WIDTH_F / 2.0, HEIGHT_F / 2.0);
}
}
if let Some(t) = &mut self.dbl_click_timeout {
*t += dt;
if *t > 300.0 {
self.dbl_click_timeout = None;
}
}
self.player.pos += (self.move_to - self.player.pos) / 20.0;
self.player_particles.host_rect.pos = self.player.pos;
self.joining_particles.particle_system.host_rect.pos +=
(self.player.pos - self.joining_particles.particle_system.host_rect.pos) / 30.0;
self.camera.pos +=
(self.player.pos - Vector::new(WIDTH_F / 2.0, HEIGHT_F / 2.0) - self.camera.pos) / 40.0;
window.set_view(View::new(self.camera));
self.player_r += dt / 10.0;
if self.state_dirty {
self.state_dirty = false;
if self.state > 1 && !self.music_on {
let mut music_on = false;
self.music2.execute(|m2| {
music_on = true;
m2.set_volume(0.6);
m2.play()
})?;
if music_on {
self.music_on = true;
self.music_timer = 0.0;
}
}
match self.state {
1 => {
self.menu = Menu::s_01();
self.current_finished = false;
self.selection_mode = false;
}
2 => {
self.menu = Menu::s_02();
self.current_finished = false;
self.selection_mode = true;
}
3 => {
self.menu = Menu::s_03();
self.current_finished = false;
self.selection_mode = false;
}
4 => {
self.menu = Menu::s_04();
self.current_finished = false;
self.selection_mode = false;
}
5 => {
self.menu = Menu::s_05();
self.current_finished = false;
self.selection_mode = false;
}
6 => {
self.menu = Menu::s_06();
self.current_finished = false;
self.selection_mode = false;
}
7 => {
self.menu = Menu::s_07();
self.current_finished = false;
self.selection_mode = false;
}
8 => {
self.menu = Menu::s_08();
self.current_finished = true;
self.selection_mode = false;
self.is_create_mode = true;
}
9 => {
self.menu = Menu::s_09();
self.current_finished = false;
self.selection_mode = false;
self.is_create_mode = false;
}
10 => {
self.menu = Menu::s_10();
self.current_finished = false;
self.selection_mode = false;
self.is_create_mode = true;
}
_ => {
self.menu = Menu::start();
self.current_item = None;
self.selection_mode = true;
self.is_create_mode = false;
self.state = 0;
self.player_particles.opacity = 0.0;
self.joining_particles.particle_system.opacity = 0.0;
self.expl_conv_p_systems.clear();
self.planets.clear();
self.stars.clear();
self.player.pos = Vector::new(WIDTH_F / 2.0, HEIGHT_F / 2.0);
self.move_to = Vector::new(WIDTH_F / 2.0, HEIGHT_F / 2.0);
self.camera.pos = Vector::new(0.0, 0.0);
self.click_time = None;
}
}
}
if self.joining_particles.particle_system.opacity < 1.0 && self.state > 2 {
self.joining_particles.particle_system.opacity += JOINING_OPACITY_RATE * dt as f32;
if self.joining_particles.particle_system.opacity > 1.0 {
self.joining_particles.particle_system.opacity = 1.0;
}
self.joining_particles.offset =
(1.0 - self.joining_particles.particle_system.opacity / 1.0) * JOINING_FAR_DIST
+ self.joining_particles.particle_system.opacity / 1.0 * JOINING_NEAR_DIST;
}
if self.player_particles.opacity < 1.0 && self.state > 1 {
self.player_particles.opacity += dt as f32 / 7000.0;
if self.player_particles.opacity > 1.0 {
self.player_particles.opacity = 1.0;
}
}
if self.music_on {
self.music_timer += dt;
if self.music_timer > MUSIC2_LENGTH {
self.music_timer = 0.0;
self.music2.execute(|m2| m2.play())?;
}
} else if self.state == 10 {
let mut music_on = false;
self.music2.execute(|m2| {
music_on = true;
m2.set_volume(0.6);
m2.play()
})?;
if music_on {
self.music_on = true;
self.music_timer = 0.0;
}
}
for i in 0..self.menu.items.len() {
let mi: &mut MenuItem = &mut self.menu.items[i];
if !mi.is_loaded {
match &mut mi.item_type {
MenuItemType::Button {
text,
text_image,
text_c,
h_c,
c,
} => {
self.font.execute(|font| {
let style = FontStyle::new(42.0, *text_c);
*text_image = Some(font.render(text, &style)?);
Ok(())
})?;
if text_image.is_some() {
mi.is_loaded = true;
if i + 1 < self.menu.items.len() {
self.menu.items[i + 1].is_loaded = false;
} else {
self.current_finished = true;
}
}
}
MenuItemType::AppearingText {
text,
text_image,
current_text,
text_size,
text_c,
timer,
} => {
*timer += dt;
if *timer > TEXT_RATE {
*timer -= TEXT_RATE;
let next = text.chars().nth(current_text.len());
if let Some(next_t) = next {
current_text.push(next_t);
self.s_tap.execute(|s| {
s.set_volume(0.2);
s.play()
})?;
} else {
mi.is_loaded = true;
if i + 1 < self.menu.items.len() {
self.menu.items[i + 1].is_loaded = false;
} else {
self.current_finished = true;
}
continue;
}
self.font.execute(|font| {
let style = FontStyle::new(*text_size, *text_c);
*text_image = Some(font.render(current_text, &style)?);
Ok(())
})?;
}
}
MenuItemType::Pause { timer, length } => {
*timer += dt;
if timer > length {
mi.is_loaded = true;
if i + 1 < self.menu.items.len() {
self.menu.items[i + 1].is_loaded = false;
} else {
self.current_finished = true;
}
}
}
MenuItemType::InstantText {
text,
text_image,
text_size,
text_color,
} => {
if text_image.is_none() {
self.font.execute(|f| {
let style = FontStyle::new(*text_size, *text_color);
*text_image = Some(f.render(text, &style)?);
Ok(())
})?;
}
if text_image.is_some() {
mi.is_loaded = true;
if i + 1 < self.menu.items.len() {
self.menu.items[i + 1].is_loaded = false;
} else {
self.current_finished = true;
}
}
}
}
}
}
self.player_particles.host_rect = self.player;
self.player_particles.update(dt);
self.joining_particles.update(dt);
for i in (0..self.expl_conv_p_systems.len()).rev() {
if self.expl_conv_p_systems[i].update(dt, &mut self.planets) {
self.expl_conv_p_systems.swap_remove(i);
}
}
for planet in &mut self.planets {
planet.update(dt);
}
for star in &mut self.stars {
star.update(dt);
}
if let Some(sl) = &mut self.save_load_notification {
match sl {
SaveLoadNotification::Save { text, timer } => {
*timer -= dt;
if *timer <= 0.0 {
self.save_load_notification = None;
} else if text.is_none() {
self.font.execute(|f| {
*text = Some(
f.render("Saved the Game", &FontStyle::new(45.0, Color::WHITE))?,
);
Ok(())
})?;
}
}
SaveLoadNotification::Load { text, timer } => {
*timer -= dt;
if *timer <= 0.0 {
self.save_load_notification = None;
} else if text.is_none() {
self.font.execute(|f| {
*text = Some(
f.render("Loaded the Game", &FontStyle::new(45.0, Color::WHITE))?,
);
Ok(())
})?;
}
}
}
}
if self.i_star_actual.is_none() {
if let Some(i_s) = &mut self.i_star {
let mut star: Option<Image> = None;
i_s.execute(|i| {
star = Some(i.clone());
Ok(())
})?;
self.i_star_actual = star;
}
if self.i_star_actual.is_some() {
self.i_star = None;
}
}
Ok(())
}
fn draw(&mut self, window: &mut Window) -> Result<()> {
window.clear(Color::BLACK)?;
let mut rect = Rectangle::default();
for mi in &mut self.menu.items {
rect.pos.x = mi.x;
rect.pos.y = mi.y;
rect.size.x = mi.w;
rect.size.y = mi.h;
match &mut mi.item_type {
MenuItemType::Button {
text,
text_image,
text_c,
h_c,
c,
} => {
if mi.is_hover {
window.draw(&rect, Col(*h_c));
} else {
window.draw(&rect, Col(*c));
}
if let Some(i) = text_image {
let mut image_rect = i.area();
image_rect.pos.x = mi.x + (mi.w - image_rect.size.x) / 2.0;
image_rect.pos.y = mi.y + (mi.h - image_rect.size.y) / 2.0;
window.draw(&image_rect, Img(i));
}
}
MenuItemType::AppearingText {
text,
text_image,
current_text,
text_size,
text_c,
timer,
} => {
if let Some(i) = text_image {
let mut image_rect = i.area();
image_rect.pos.x = mi.x;
image_rect.pos.y = mi.y;
window.draw(&image_rect, Img(i));
}
}
MenuItemType::InstantText {
text,
text_image,
text_size,
text_color,
} => {
if let Some(i) = text_image {
let mut image_rect = i.area();
image_rect.pos.x = mi.x;
image_rect.pos.y = mi.y;
window.draw(&image_rect, Img(i));
}
}
MenuItemType::Pause { timer, length } => (),
}
}
self.player_particles.draw(window, Transform::IDENTITY);
window.draw_ex(
&self.player,
Col(Color::from_rgba(
0xFF,
0xFF,
0xFF,
self.player_particles.opacity,
)),
Transform::translate((-self.player.size.x / 2.0, -self.player.size.y / 2.0))
* Transform::rotate(self.player_r as f32),
1,
);
self.joining_particles.draw(window, Transform::IDENTITY);
for expl_conv_ps in &mut self.expl_conv_p_systems {
expl_conv_ps.draw(window, Transform::IDENTITY);
}
for planet in &mut self.planets {
planet.draw(window, Transform::IDENTITY);
}
if let Some(i) = &mut self.i_star_actual {
for star in &mut self.stars {
star.draw(i, window, Transform::IDENTITY);
}
}
if let Some(sl) = &mut self.save_load_notification {
match sl {
SaveLoadNotification::Save { text, timer }
| SaveLoadNotification::Load { text, timer } => {
if let Some(i) = text {
let mut c = Color::WHITE;
c.a = (*timer / SL_NOTIF_TIME) as f32;
let mut image_rect = i.area();
image_rect.pos = self.camera.pos + Vector::new(20.0, 20.0);
window.draw(&image_rect, Blended(i, c));
}
}
}
}
Ok(())
}
}
fn main() {
run::<GameState>(
"One And All - a Ludum Dare 45 compo entry",
Vector::new(800, 600),
Settings::default(),
);
}