1use micro_games_kit::{
2 assets::{make_directory_database, ShaderAsset},
3 config::Config,
4 context::GameContext,
5 game::{GameInstance, GameState},
6 grid_world::{GridWorld, GridWorldLayer},
7 pcg::{Grid, NoiseGenerator, RemapGenerator, SubGenerator},
8 third_party::{
9 noise::{Fbm, MultiFractal, NoiseFn, SuperSimplex},
10 raui_immediate_widgets::core::{
11 text_box, TextBoxFont, TextBoxHorizontalAlign, TextBoxProps,
12 },
13 spitfire_draw::{
14 tiles::{TileInstance, TileMap, TileSet, TileSetItem, TilesEmitter},
15 utils::{Drawable, ShaderRef},
16 },
17 spitfire_glow::graphics::{CameraScaling, Shader},
18 spitfire_input::{ArrayInputCombinator, InputAxisRef, InputMapping, VirtualAxis},
19 vek::{Rgba, Vec2},
20 },
21 GameLauncher,
22};
23use std::{
24 array::from_fn,
25 error::Error,
26 ops::{Add, Div, Mul, RangeInclusive, Sub},
27};
28
29const SIZE: usize = 50;
30const WATER: usize = 0;
31const FOREST: usize = 1;
32const GRASS: usize = 2;
33const SAND: usize = 3;
34const ROCK: usize = 4;
35const SNOW: usize = 5;
36const WIND: Vec2<f64> = Vec2 { x: 1.0, y: 0.5 };
37
38const CLEAR_SKY: usize = 0;
39const CLOUD_SKY: usize = 1;
40const RAINY_SKY: usize = 2;
41
42struct State {
43 world: GridWorld,
44 weather_tileset: TileSet,
45 weather_noise: Fbm<SuperSimplex>,
46 time: f64,
47 mouse_position: ArrayInputCombinator<2>,
48}
49
50impl Default for State {
51 fn default() -> Self {
52 let mut height = Grid::<f64>::generate(
53 SIZE.into(),
54 NoiseGenerator::new(Fbm::<SuperSimplex>::default().set_frequency(0.025)),
55 );
56 height.apply_all(RemapGenerator {
57 from: -1.0..1.0,
58 to: 0.0..1.0,
59 });
60
61 let gradient = Grid::<f64>::generate(
62 SIZE.into(),
63 |location: Vec2<usize>, size: Vec2<usize>, _| {
64 let center = size / 2;
65 let x = if location.x >= center.x {
66 location.x - center.x
67 } else {
68 center.x - location.x
69 } as f64;
70 let y = if location.y >= center.y {
71 location.y - center.y
72 } else {
73 center.y - location.y
74 } as f64;
75 let result = (x / center.x as f64).max(y / center.y as f64);
76 result * result
77 },
78 );
79 height.apply_all(SubGenerator { other: &gradient });
80
81 let mut biome = Grid::<f64>::generate(
82 SIZE.into(),
83 NoiseGenerator::new(Fbm::<SuperSimplex>::new(42).set_frequency(0.05)),
84 );
85 biome.apply_all(RemapGenerator {
86 from: -1.0..1.0,
87 to: 0.0..1.0,
88 });
89
90 let buffer = height
91 .into_inner()
92 .1
93 .into_iter()
94 .zip(biome.into_inner().1)
95 .map(|(height, biome)| {
96 if height > 0.75 {
97 SNOW
98 } else if height > 0.6 {
99 ROCK
100 } else if height > 0.1 {
101 if biome > 0.8 {
102 SAND
103 } else if biome > 0.5 {
104 GRASS
105 } else {
106 FOREST
107 }
108 } else {
109 WATER
110 }
111 })
112 .collect();
113
114 Self {
115 world: GridWorld::new(
116 10.0.into(),
117 TileSet::default()
118 .shader(ShaderRef::name("color"))
119 .mapping(WATER, TileSetItem::default().tint(Rgba::blue()))
120 .mapping(
121 FOREST,
122 TileSetItem::default().tint(Rgba::new_opaque(0.0, 0.5, 0.0)),
123 )
124 .mapping(GRASS, TileSetItem::default().tint(Rgba::green()))
125 .mapping(
126 SAND,
127 TileSetItem::default().tint(Rgba::new_opaque(1.0, 1.0, 0.5)),
128 )
129 .mapping(ROCK, TileSetItem::default().tint(Rgba::gray(0.5)))
130 .mapping(SNOW, TileSetItem::default().tint(Rgba::white())),
131 GridWorldLayer::new(TileMap::with_buffer(SIZE.into(), buffer).unwrap()),
132 ),
133 weather_tileset: TileSet::default()
134 .shader(ShaderRef::name("color"))
135 .mapping(
136 CLEAR_SKY,
137 TileSetItem::default().tint(Rgba::new(1.0, 1.0, 1.0, 0.0)),
138 )
139 .mapping(
140 CLOUD_SKY,
141 TileSetItem::default().tint(Rgba::new(1.0, 1.0, 1.0, 0.8)),
142 )
143 .mapping(
144 RAINY_SKY,
145 TileSetItem::default().tint(Rgba::new(0.3, 0.3, 0.3, 0.8)),
146 ),
147 weather_noise: Fbm::<SuperSimplex>::default().set_frequency(0.03),
148 time: 0.0,
149 mouse_position: Default::default(),
150 }
151 }
152}
153
154impl GameState for State {
155 fn enter(&mut self, context: GameContext) {
156 context.graphics.main_camera.scaling = CameraScaling::FitVertical(SIZE as f32 * 10.0);
157
158 context
159 .assets
160 .spawn(
161 "shader://color",
162 (ShaderAsset::new(
163 Shader::COLORED_VERTEX_2D,
164 Shader::PASS_FRAGMENT,
165 ),),
166 )
167 .unwrap();
168 context
169 .assets
170 .spawn(
171 "shader://text",
172 (ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
173 )
174 .unwrap();
175
176 context.assets.ensure("font://roboto.ttf").unwrap();
177
178 let mouse_x = InputAxisRef::default();
179 let mouse_y = InputAxisRef::default();
180 self.mouse_position = ArrayInputCombinator::new([mouse_x.clone(), mouse_y.clone()]);
181 context.input.push_mapping(
182 InputMapping::default()
183 .axis(VirtualAxis::MousePositionX, mouse_x)
184 .axis(VirtualAxis::MousePositionY, mouse_y),
185 );
186 }
187
188 fn fixed_update(&mut self, _: GameContext, delta_time: f32) {
189 self.time += delta_time as f64;
190 }
191
192 fn draw(&mut self, context: GameContext) {
193 self.world.draw(context.draw, context.graphics);
194
195 TilesEmitter::default()
196 .tile_size(10.0.into())
197 .emit(
198 &self.weather_tileset,
199 (0..SIZE)
200 .flat_map(|y| (0..SIZE).map(move |x| (x, y)))
201 .map(|(x, y)| TileInstance {
202 id: self.weather(x, y, self.time),
203 location: Vec2 { x, y },
204 }),
205 )
206 .draw(context.draw, context.graphics);
207 }
208
209 fn draw_gui(&mut self, context: GameContext) {
210 let size = context
211 .graphics
212 .main_camera
213 .scaling
214 .world_size(context.graphics.main_camera.screen_size);
215 let [x, y] = self.mouse_position.get();
216 let x = remap(
217 x,
218 0.0..=context.graphics.main_camera.screen_size.x,
219 0.0..=size.x,
220 );
221 let y = remap(
222 y,
223 0.0..=context.graphics.main_camera.screen_size.y,
224 0.0..=size.y,
225 );
226 let x = ((x / 10.0) as usize).min(SIZE.saturating_sub(1));
227 let y = ((y / 10.0) as usize).min(SIZE.saturating_sub(1));
228
229 let forecast = from_fn::<&str, 3, _>(|index| {
230 match self.weather(x, y, self.time + index as f64 * 5.0) {
231 0 => "Clear sky",
232 1 => "Clouds",
233 2 => "Rain",
234 _ => "<unknown>",
235 }
236 });
237
238 text_box(TextBoxProps {
239 text: format!(
240 "Tile: {} x {}\nTime: {:.2}\nForecast:\n+0s: {}\n+5s: {}\n+10s: {}",
241 x, y, self.time, forecast[0], forecast[1], forecast[2]
242 ),
243 horizontal_align: TextBoxHorizontalAlign::Right,
244 font: TextBoxFont {
245 name: "roboto.ttf".to_owned(),
246 size: 32.0,
247 },
248 ..Default::default()
249 });
250 }
251}
252
253impl State {
254 fn weather(&self, x: usize, y: usize, time: f64) -> usize {
255 let x = x as f64 + WIND.x * time;
256 let y = y as f64 + WIND.y * time;
257 let sample = self.weather_noise.get([x, y, time]);
258 let sample = remap(sample, -0.5..=1.0, 0.0..=3.0);
259 (sample as usize).clamp(0, 2)
260 }
261}
262
263fn remap<T: Copy + Sub<Output = T> + Div<Output = T> + Add<Output = T> + Mul<Output = T>>(
264 value: T,
265 from: RangeInclusive<T>,
266 to: RangeInclusive<T>,
267) -> T {
268 let from_start = *from.start();
269 let from_end = *from.end();
270 let to_start = *to.start();
271 let to_end = *to.end();
272 let factor = (value - from_start) / (from_end - from_start);
273 (to_end - to_start) * factor + to_start
274}
275
276fn main() -> Result<(), Box<dyn Error>> {
277 GameLauncher::new(GameInstance::new(State::default()).setup_assets(|assets| {
278 *assets = make_directory_database("./resources/").unwrap();
279 }))
280 .title("Procedural Content Generator - Island")
281 .config(Config::load_from_file("./resources/GameConfig.toml")?)
282 .run();
283 Ok(())
284}