pcg_island/
pcg_island.rs

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}