Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions src/launching.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use bevy::input::common_conditions::{input_just_pressed, input_just_released};
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy::input::touch::{TouchInput, TouchPhase};
use crate::satellite_names_advanced::{SatelliteSpawnStats, codename_for_satellite, SatelliteLabel, LabelFade};

#[derive(Component)]
pub struct LaunchPad;
Expand Down Expand Up @@ -99,6 +100,7 @@ fn start_new_launch(
mut score: ResMut<Score>,
satellite_price_factor: Res<SatellitePriceFactor>,
current_marked: Query<Entity, With<NavigationInstruments>>,
mut spawn_stats: ResMut<SatelliteSpawnStats>,
) {

let Some(launch_pad_transform) = launch_pad_query.iter().next() else { return; };
Expand Down Expand Up @@ -146,6 +148,21 @@ fn start_new_launch(
} else {
return;
}

// Update spawn statistics
spawn_stats.global_spawn_idx = spawn_stats.global_spawn_idx.wrapping_add(1);
let level_key = lvl as u32;
let level_counter = spawn_stats.per_level_spawn_idx.entry(level_key).or_insert(0);
*level_counter = level_counter.saturating_add(1);

// Determine codename
let maybe_codename = codename_for_satellite(
lvl,
*level_counter,
spawn_stats.global_spawn_idx,
&mut spawn_stats,
);

// Ensure only the newly launched satellite will be selected
for e in current_marked.iter() {
commands.entity(e).remove::<NavigationInstruments>();
Expand Down Expand Up @@ -200,6 +217,30 @@ let collector_id = commands.spawn((
Visibility::Visible,
Pickable::IGNORE,
));

// Spawn label with fade-in animation if satellite has a codename
if let Some(codename) = maybe_codename {
commands.spawn((
Text2d::new(codename),
Transform::default()
.with_translation(Vec3::new(0.0, -800.0, 1.0))
.with_scale(Vec3::splat(10.0)),
TextFont {
font: solar_system_assets.font.clone(),
font_size: 18.0,
..default()
},
TextColor(Color::srgba(1.0, 1.0, 1.0, 0.0)), // Start transparent
ChildOf(collector_id),
SatelliteLabel,
LabelFade {
elapsed: 0.0,
duration: 0.5,
fade_in: true,
},
Pickable::IGNORE,
));
}

launch_state.launched_at_time = None;
}
Expand Down Expand Up @@ -246,7 +287,8 @@ fn start_launch_from_touch_end(
mut score: ResMut<Score>,
current_marked: Query<Entity, With<NavigationInstruments>>,
time: Res<Time>,
price: Res<SatellitePriceFactor>
price: Res<SatellitePriceFactor>,
mut spawn_stats: ResMut<SatelliteSpawnStats>,
) {
let Some(launch_pad_transform) = launch_pad_query.iter().next() else { return; };
let launch_position = launch_pad_transform.translation;
Expand Down Expand Up @@ -295,6 +337,21 @@ fn start_launch_from_touch_end(
} else {
return;
}

// Update spawn statistics
spawn_stats.global_spawn_idx = spawn_stats.global_spawn_idx.wrapping_add(1);
let level_key = lvl as u32;
let level_counter = spawn_stats.per_level_spawn_idx.entry(level_key).or_insert(0);
*level_counter = level_counter.saturating_add(1);

// Determine codename
let maybe_codename = codename_for_satellite(
lvl,
*level_counter,
spawn_stats.global_spawn_idx,
&mut spawn_stats,
);

// Ensure only the newly launched satellite will be selected
for e in current_marked.iter() {
commands.entity(e).remove::<NavigationInstruments>();
Expand Down Expand Up @@ -349,6 +406,30 @@ fn start_launch_from_touch_end(
Visibility::Visible,
Pickable::IGNORE,
));

// Spawn label with fade-in animation if satellite has a codename
if let Some(codename) = maybe_codename {
commands.spawn((
Text2d::new(codename),
Transform::default()
.with_translation(Vec3::new(0.0, -800.0, 1.0))
.with_scale(Vec3::splat(10.0)),
TextFont {
font: solar_system_assets.font.clone(),
font_size: 18.0,
..default()
},
TextColor(Color::srgba(1.0, 1.0, 1.0, 0.0)), // Start transparent
ChildOf(collector_id),
SatelliteLabel,
LabelFade {
elapsed: 0.0,
duration: 0.5,
fade_in: true,
},
Pickable::IGNORE,
));
}

// disarm after launch
launch_armed.0 = false;
Expand Down Expand Up @@ -528,5 +609,4 @@ fn sun_thruster_touch(
_ => {}
}
}
}

}
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod sound;
mod trails;
mod effects;
mod achievements;
mod satellite_names_advanced;

use std::ops::{Deref, DerefMut};
use crate::screens::Screen;
Expand Down Expand Up @@ -78,6 +79,7 @@ impl Plugin for AppPlugin {
sound::SoundPlugin,
trails::TrailsPlugin,
achievements::AchievementsPlugin,
satellite_names_advanced::plugin,
));
// Tell bevy that our AppSystems should always be executed in the below order
app.configure_sets(
Expand Down Expand Up @@ -154,4 +156,4 @@ impl DerefMut for RandomSource {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
}
139 changes: 139 additions & 0 deletions src/satellite_names_advanced.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use bevy::prelude::*;
use crate::GameplaySystem;
use crate::sun_system::{Satellite, Level};
use crate::screens::gameplay::CameraZoom;
use std::collections::HashMap;

#[derive(Component)]
pub struct SatelliteLabel;

#[derive(Component)]
pub struct LabelFade {
pub elapsed: f32,
pub duration: f32,
pub fade_in: bool,
}

#[derive(Default, Resource)]
pub struct SatelliteSpawnStats {
pub global_spawn_idx: u64,
pub per_level_spawn_idx: HashMap<u32, u32>,
pub japanese_name_cursor: usize,
}

#[derive(Resource)]
struct LabelPool {
available: Vec<Entity>,
in_use: HashMap<Entity, Entity>, // satellite -> label
}

impl Default for LabelPool {
fn default() -> Self {
Self {
available: Vec::new(),
in_use: HashMap::new(),
}
}
}

// Fixed codenames for the first satellite per level
fn first_of_level_codename(level: f32) -> Option<&'static str> {
match level as i32 {
1 => Some("tiny-1"),
2 => Some("traily"),
3 => Some("boinc"),
_ => None,
}
}

// Japanese codenames (using romaji for ASCII compatibility)
static JAPANESE_CODENAMES: &[&str] = &[
"Sakura", "Kitsune", "Kumo", "Hoshi", "Tsuki",
"Hikari", "Yami", "Kaze", "Mizu", "Inazuma",
"Tora", "Ryuu", "Kaguya", "Akari", "Kuro",
];

fn next_japanese_codename(stats: &mut SatelliteSpawnStats) -> &'static str {
let name = JAPANESE_CODENAMES[stats.japanese_name_cursor % JAPANESE_CODENAMES.len()];
stats.japanese_name_cursor = stats.japanese_name_cursor.wrapping_add(1);
name
}

pub fn codename_for_satellite(
level: f32,
level_spawn_idx: u32,
global_spawn_idx: u64,
stats: &mut SatelliteSpawnStats,
) -> Option<String> {
if level_spawn_idx == 1 {
return first_of_level_codename(level).map(|s| s.to_string());
}
if global_spawn_idx % 10 == 0 {
return Some(next_japanese_codename(stats).to_string());
}
None
}

pub fn plugin(app: &mut App) {
app.init_resource::<SatelliteSpawnStats>();
app.init_resource::<LabelPool>();
app.add_systems(Update, (
update_label_visibility,
animate_label_fade,
cleanup_orphaned_labels,
).in_set(GameplaySystem));
}

fn update_label_visibility(
camera_query: Query<&CameraZoom>,
mut label_query: Query<&mut Visibility, With<SatelliteLabel>>,
) {
let Ok(zoom) = camera_query.single() else { return; };
// Only show labels when zoomed in (level 0-2 out of 0-4)
let show_labels = zoom.level <= 2;

for mut visibility in label_query.iter_mut() {
*visibility = if show_labels {
Visibility::Inherited
} else {
Visibility::Hidden
};
}
}

fn animate_label_fade(
mut commands: Commands,
mut query: Query<(Entity, &mut LabelFade, &mut TextColor)>,
time: Res<Time>,
) {
for (entity, mut fade, mut color) in query.iter_mut() {
fade.elapsed += time.delta_secs();
let t = (fade.elapsed / fade.duration).clamp(0.0, 1.0);

let alpha = if fade.fade_in { t } else { 1.0 - t };
color.0 = color.0.with_alpha(alpha * 0.9);

if fade.elapsed >= fade.duration {
commands.entity(entity).remove::<LabelFade>();

// If fading out, hide the label
if !fade.fade_in {
commands.entity(entity).insert(Visibility::Hidden);
}
}
}
}

fn cleanup_orphaned_labels(
mut commands: Commands,
label_query: Query<(Entity, &Parent), With<SatelliteLabel>>,
satellite_query: Query<(), With<Satellite>>,
) {
for (label_entity, parent) in label_query.iter() {
// Check if parent satellite still exists
if satellite_query.get(parent.get()).is_err() {
// Parent satellite was destroyed, despawn label
commands.entity(label_entity).despawn();
}
}
}