use micro_games_kit::{
assets::{make_directory_database, ShaderAsset},
config::Config,
context::GameContext,
game::{GameInstance, GameState},
grid_world::{GridWorld, GridWorldLayer},
pcg::{Grid, NoiseGenerator, RemapGenerator, SubGenerator},
third_party::{
noise::{Fbm, MultiFractal, NoiseFn, SuperSimplex},
raui_immediate_widgets::core::{
text_box, TextBoxFont, TextBoxHorizontalAlign, TextBoxProps,
},
spitfire_draw::{
tiles::{TileInstance, TileMap, TileSet, TileSetItem, TilesEmitter},
utils::{Drawable, ShaderRef},
},
spitfire_glow::graphics::{CameraScaling, Shader},
spitfire_input::{ArrayInputCombinator, InputAxisRef, InputMapping, VirtualAxis},
vek::{Rgba, Vec2},
},
GameLauncher,
};
use std::{
array::from_fn,
error::Error,
ops::{Add, Div, Mul, RangeInclusive, Sub},
};
const SIZE: usize = 50;
const WATER: usize = 0;
const FOREST: usize = 1;
const GRASS: usize = 2;
const SAND: usize = 3;
const ROCK: usize = 4;
const SNOW: usize = 5;
const WIND: Vec2<f64> = Vec2 { x: 1.0, y: 0.5 };
const CLEAR_SKY: usize = 0;
const CLOUD_SKY: usize = 1;
const RAINY_SKY: usize = 2;
struct State {
world: GridWorld,
weather_tileset: TileSet,
weather_noise: Fbm<SuperSimplex>,
time: f64,
mouse_position: ArrayInputCombinator<2>,
}
impl Default for State {
fn default() -> Self {
let mut height = Grid::<f64>::generate(
SIZE.into(),
NoiseGenerator::new(Fbm::<SuperSimplex>::default().set_frequency(0.025)),
);
height.apply_all(RemapGenerator {
from: -1.0..1.0,
to: 0.0..1.0,
});
let gradient = Grid::<f64>::generate(
SIZE.into(),
|location: Vec2<usize>, size: Vec2<usize>, _| {
let center = size / 2;
let x = if location.x >= center.x {
location.x - center.x
} else {
center.x - location.x
} as f64;
let y = if location.y >= center.y {
location.y - center.y
} else {
center.y - location.y
} as f64;
let result = (x / center.x as f64).max(y / center.y as f64);
result * result
},
);
height.apply_all(SubGenerator { other: &gradient });
let mut biome = Grid::<f64>::generate(
SIZE.into(),
NoiseGenerator::new(Fbm::<SuperSimplex>::new(42).set_frequency(0.05)),
);
biome.apply_all(RemapGenerator {
from: -1.0..1.0,
to: 0.0..1.0,
});
let buffer = height
.into_inner()
.1
.into_iter()
.zip(biome.into_inner().1)
.map(|(height, biome)| {
if height > 0.75 {
SNOW
} else if height > 0.6 {
ROCK
} else if height > 0.1 {
if biome > 0.8 {
SAND
} else if biome > 0.5 {
GRASS
} else {
FOREST
}
} else {
WATER
}
})
.collect();
Self {
world: GridWorld::new(
10.0.into(),
TileSet::default()
.shader(ShaderRef::name("color"))
.mapping(WATER, TileSetItem::default().tint(Rgba::blue()))
.mapping(
FOREST,
TileSetItem::default().tint(Rgba::new_opaque(0.0, 0.5, 0.0)),
)
.mapping(GRASS, TileSetItem::default().tint(Rgba::green()))
.mapping(
SAND,
TileSetItem::default().tint(Rgba::new_opaque(1.0, 1.0, 0.5)),
)
.mapping(ROCK, TileSetItem::default().tint(Rgba::gray(0.5)))
.mapping(SNOW, TileSetItem::default().tint(Rgba::white())),
GridWorldLayer::new(TileMap::with_buffer(SIZE.into(), buffer).unwrap()),
),
weather_tileset: TileSet::default()
.shader(ShaderRef::name("color"))
.mapping(
CLEAR_SKY,
TileSetItem::default().tint(Rgba::new(1.0, 1.0, 1.0, 0.0)),
)
.mapping(
CLOUD_SKY,
TileSetItem::default().tint(Rgba::new(1.0, 1.0, 1.0, 0.8)),
)
.mapping(
RAINY_SKY,
TileSetItem::default().tint(Rgba::new(0.3, 0.3, 0.3, 0.8)),
),
weather_noise: Fbm::<SuperSimplex>::default().set_frequency(0.03),
time: 0.0,
mouse_position: Default::default(),
}
}
}
impl GameState for State {
fn enter(&mut self, context: GameContext) {
context.graphics.main_camera.scaling = CameraScaling::FitVertical(SIZE as f32 * 10.0);
context
.assets
.spawn(
"shader://color",
(ShaderAsset::new(
Shader::COLORED_VERTEX_2D,
Shader::PASS_FRAGMENT,
),),
)
.unwrap();
context
.assets
.spawn(
"shader://text",
(ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
)
.unwrap();
context.assets.ensure("font://roboto.ttf").unwrap();
let mouse_x = InputAxisRef::default();
let mouse_y = InputAxisRef::default();
self.mouse_position = ArrayInputCombinator::new([mouse_x.clone(), mouse_y.clone()]);
context.input.push_mapping(
InputMapping::default()
.axis(VirtualAxis::MousePositionX, mouse_x)
.axis(VirtualAxis::MousePositionY, mouse_y),
);
}
fn fixed_update(&mut self, _: GameContext, delta_time: f32) {
self.time += delta_time as f64;
}
fn draw(&mut self, context: GameContext) {
self.world.draw(context.draw, context.graphics);
TilesEmitter::default()
.tile_size(10.0.into())
.emit(
&self.weather_tileset,
(0..SIZE)
.flat_map(|y| (0..SIZE).map(move |x| (x, y)))
.map(|(x, y)| TileInstance {
id: self.weather(x, y, self.time),
location: Vec2 { x, y },
}),
)
.draw(context.draw, context.graphics);
}
fn draw_gui(&mut self, context: GameContext) {
let size = context
.graphics
.main_camera
.scaling
.world_size(context.graphics.main_camera.screen_size);
let [x, y] = self.mouse_position.get();
let x = remap(
x,
0.0..=context.graphics.main_camera.screen_size.x,
0.0..=size.x,
);
let y = remap(
y,
0.0..=context.graphics.main_camera.screen_size.y,
0.0..=size.y,
);
let x = ((x / 10.0) as usize).min(SIZE.saturating_sub(1));
let y = ((y / 10.0) as usize).min(SIZE.saturating_sub(1));
let forecast = from_fn::<&str, 3, _>(|index| {
match self.weather(x, y, self.time + index as f64 * 5.0) {
0 => "Clear sky",
1 => "Clouds",
2 => "Rain",
_ => "<unknown>",
}
});
text_box(TextBoxProps {
text: format!(
"Tile: {} x {}\nTime: {:.2}\nForecast:\n+0s: {}\n+5s: {}\n+10s: {}",
x, y, self.time, forecast[0], forecast[1], forecast[2]
),
horizontal_align: TextBoxHorizontalAlign::Right,
font: TextBoxFont {
name: "roboto.ttf".to_owned(),
size: 32.0,
},
..Default::default()
});
}
}
impl State {
fn weather(&self, x: usize, y: usize, time: f64) -> usize {
let x = x as f64 + WIND.x * time;
let y = y as f64 + WIND.y * time;
let sample = self.weather_noise.get([x, y, time]);
let sample = remap(sample, -0.5..=1.0, 0.0..=3.0);
(sample as usize).clamp(0, 2)
}
}
fn remap<T: Copy + Sub<Output = T> + Div<Output = T> + Add<Output = T> + Mul<Output = T>>(
value: T,
from: RangeInclusive<T>,
to: RangeInclusive<T>,
) -> T {
let from_start = *from.start();
let from_end = *from.end();
let to_start = *to.start();
let to_end = *to.end();
let factor = (value - from_start) / (from_end - from_start);
(to_end - to_start) * factor + to_start
}
fn main() -> Result<(), Box<dyn Error>> {
GameLauncher::new(GameInstance::new(State::default()).setup_assets(|assets| {
*assets = make_directory_database("./resources/").unwrap();
}))
.title("Procedural Content Generator - Island")
.config(Config::load_from_file("./resources/GameConfig.toml")?)
.run();
Ok(())
}