Compare commits

19 Commits

Author SHA1 Message Date
454124a90a just making sure the repo is up to date 2025-11-28 19:17:59 -06:00
4871a4ed3e Merge pull request 'procgen' (#3) from procgen into develop
Reviewed-on: #3
2025-03-02 21:33:12 +00:00
f73f5d4950 player tweaks 2025-03-02 15:32:42 -06:00
2d79747a82 Tweaked terrain values for bigger biomes and more tile variation
Still need to figure out how to get more dead bushes tho.....
2025-03-02 13:52:01 -06:00
bd2130dfa0 Fixed a lot of bad math causing issues in negative chunks 2025-03-02 13:33:43 -06:00
cc339b9389 fewer deserts 2025-03-01 23:10:22 -06:00
2be653504a I may have used ai for some of the proc gen.. 2025-03-01 22:52:34 -06:00
8c0601c7aa Basic proc gen with hash noise 2025-03-01 15:19:53 -06:00
bdf4f7a7b6 Merge pull request 'Handling interactions' (#2) from interaction into master
Reviewed-on: #2
2025-03-01 18:52:52 +00:00
22f50d04e7 Handling interactions 2025-03-01 12:46:04 -06:00
f29fad7168 Merge pull request 'Chunking system, save/load chunks' (#1) from chunking into master
Reviewed-on: #1
2025-02-28 14:40:36 +00:00
0d25cbb38a Prepping for merge 2025-02-28 08:37:11 -06:00
07768ddf9b Removing some log statements 2025-02-27 23:09:43 -06:00
5135ec1868 Dynamically load/unload chunks around player 2025-02-27 19:02:35 -06:00
f9ea78d62e Cleanup 2025-02-27 13:51:11 -06:00
cddba06c25 Saving and loading chunks actually works now 2025-02-27 13:14:29 -06:00
c6c8864d67 Making leaps to better data serialization 2025-02-26 23:21:08 -06:00
86c0b7ca6a tests 2025-02-26 20:40:05 -06:00
f7a409f565 why are the tiles still 24bytes... 2025-02-26 18:06:51 -06:00
11 changed files with 1025 additions and 129 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
bin/
game/data

View File

@@ -0,0 +1,206 @@
package game
import "core:fmt"
import "core:math/noise"
// Fixed desert finding procedure
find_desert :: proc(seed: i64) -> (found: bool, pos: Vec2i) {
search_radius := 1000
step_size := 20 // Check every 20 blocks to speed up the search
// Track how many desert tiles we find for debugging
desert_count := 0
total_checked := 0
last_desert_pos := Vec2i{0, 0}
fmt.println("Searching for deserts with seed:", seed)
for x := -search_radius; x < search_radius; x += step_size {
for y := -search_radius; y < search_radius; y += step_size {
pos := Vec2i{x, y}
biome := get_biome_type(pos, seed)
total_checked += 1
if biome.type == .DESERT {
desert_count += 1
last_desert_pos = pos
fmt.println("Found desert at:", pos)
if desert_count <= 5 { // Only report the first few to avoid spam
// Verify by checking adjacent tiles to confirm it's not just a single glitched tile
desert_size := 0
check_radius := 3
for cx := -check_radius; cx <= check_radius; cx += 1 {
for cy := -check_radius; cy <= check_radius; cy += 1 {
check_pos := Vec2i{x + cx, y + cy}
check_biome := get_biome_type(check_pos, seed)
if check_biome.type == .DESERT {
desert_size += 1
}
}
}
fmt.println(" Desert size (in 7x7 area):", desert_size, "out of", (check_radius*2+1)*(check_radius*2+1))
}
}
}
}
// Report desert statistics
desert_percentage := f32(desert_count) / f32(total_checked) * 100.0
fmt.println("Desert statistics:")
fmt.println(" Total positions checked:", total_checked)
fmt.println(" Desert tiles found:", desert_count)
fmt.println(" Desert percentage:", desert_percentage, "%")
if desert_count > 0 {
return true, last_desert_pos // Return the last desert found
} else {
fmt.println("No desert found within search radius")
return false, Vec2i{0, 0}
}
}
// Create a biome distribution map to visualize the actual distribution
generate_biome_map :: proc(seed: i64, width: int, height: int) {
biome_counts := [BiomeType]int{}
total_tiles := width * height
fmt.println("Generating biome distribution map", width, "x", height)
// First pass - count biomes
for y := 0; y < height; y += 1 {
for x := 0; x < width; x += 1 {
// Use a different area of the world for better sampling
world_x := (x - width/2) * 20
world_y := (y - height/2) * 20
biome := get_biome_type(Vec2i{world_x, world_y}, seed)
biome_counts[biome.type] += 1
// Print a character representing each biome for a ASCII map
if y % 5 == 0 && x % 5 == 0 { // Print sparse map to fit in console
c := '?'
switch biome.type {
case .DESERT: c = 'D'
case .GRASSLAND: c = 'G'
case .FOREST: c = 'F'
case .LAKE: c = 'L'
}
fmt.print(c)
}
}
if y % 5 == 0 {
fmt.println()
}
}
// Print biome statistics
fmt.println("\nBiome Distribution:")
fmt.println(" Total area:", total_tiles, "tiles")
for biome_type, count in biome_counts {
percentage := f32(count) / f32(total_tiles) * 100.0
fmt.println(" ", biome_type, ":", count, "tiles (", percentage, "%)")
}
}
// Debug the noise distribution directly
debug_noise_values :: proc(seed: i64) {
// Import math package at the top of your file
// import "core:math"
// Collect some sample values to see the actual distribution
samples := 1000
temp_values := make([dynamic]f64, 0, samples)
moisture_values := make([dynamic]f64, 0, samples)
for i := 0; i < samples; i += 1 {
// Sample across a wide area
x := (i % 50) * 100 - 2500
y := (i / 50) * 100 - 2500
// Generate values the same way as in get_biome_type
continent_scale := 0.001
region_scale := 0.005
moisture_seed := seed + 20000
temperature_seed := seed + 30000
// Get raw noise values
moisture := noise.noise_2d(moisture_seed, {f64(x) * region_scale, f64(y) * region_scale})
temperature := noise.noise_2d(temperature_seed, {f64(x) * region_scale, f64(y) * region_scale})
// Apply the same transformations as in your get_biome_type function
// Remove this line if you don't have math imported, or replace with your own pow implementation
// temperature = math.pow(temperature * 0.5 + 0.5, 0.8) * 2.0 - 1.0
// Normalize to 0-1 range
normalized_moisture := f64(moisture * 0.5 + 0.5)
normalized_temperature := f64(temperature * 0.5 + 0.5)
append_elem(&temp_values, normalized_temperature)
append_elem(&moisture_values, normalized_moisture)
}
// Calculate statistics
temp_min, temp_max, temp_avg := 1.0, 0.0, 0.0
moisture_min, moisture_max, moisture_avg := 1.0, 0.0, 0.0
for i := 0; i < samples; i += 1 {
temp := temp_values[i]
moisture := moisture_values[i]
temp_avg += temp
moisture_avg += moisture
temp_min = min(temp_min, temp)
temp_max = max(temp_max, temp)
moisture_min = min(moisture_min, moisture)
moisture_max = max(moisture_max, moisture)
}
temp_avg /= f64(samples)
moisture_avg /= f64(samples)
// Print statistics
fmt.println("Temperature values (normalized to 0-1):")
fmt.println(" Min:", temp_min, "Max:", temp_max, "Avg:", temp_avg)
fmt.println("Moisture values (normalized to 0-1):")
fmt.println(" Min:", moisture_min, "Max:", moisture_max, "Avg:", moisture_avg)
// Count how many points would qualify as deserts with different thresholds
desert_count_strict := 0
desert_count_medium := 0
desert_count_loose := 0
for i := 0; i < samples; i += 1 {
temp := temp_values[i]
moisture := moisture_values[i]
// Strict: temp > 0.55 && moisture < 0.4
if temp > 0.55 && moisture < 0.4 {
desert_count_strict += 1
}
// Medium: temp > 0.4 && moisture < 0.6
if temp > 0.4 && moisture < 0.6 {
desert_count_medium += 1
}
// Loose: temp > 0.3 || moisture < 0.4
if temp > 0.3 || moisture < 0.4 {
desert_count_loose += 1
}
}
fmt.println("\nDesert qualification rates with different thresholds:")
fmt.println(" Strict (temp > 0.55 && moisture < 0.4):",
f32(desert_count_strict)/f32(samples)*100.0, "%")
fmt.println(" Medium (temp > 0.4 && moisture < 0.6):",
f32(desert_count_medium)/f32(samples)*100.0, "%")
fmt.println(" Loose (temp > 0.3 || moisture < 0.4):",
f32(desert_count_loose)/f32(samples)*100.0, "%")
}

BIN
game/game

Binary file not shown.

View File

@@ -2,47 +2,50 @@ package game
import "core:fmt"
import rl "vendor:raylib"
import rand "core:math/rand"
import "core:strconv"
import "core:mem"
import "core:strings"
import "core:os"
player : Player
world : World
camera : rl.Camera2D
main :: proc() {
if !os.is_dir("data") {
os.make_directory("data")
}
if !os.is_dir("data/worlds") {
os.make_directory("data/worlds")
}
rl.InitWindow(1280, 720, "Odin game")
flags : rl.ConfigFlags = {.VSYNC_HINT}
rl.SetConfigFlags(flags)
rl.SetTargetFPS(60)
player.position.x = CELL_SIZE * 5
player.position.y = CELL_SIZE * 5
player.mode = .INTERACT
camera.target = {player.position.x + (CELL_SIZE / 2), player.position.y + (CELL_SIZE / 2)}
camera.zoom = 2
camera.offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2}
player = {
position = {CELL_SIZE * 0, CELL_SIZE * 0},
camera = {
zoom = 4,
target = {player.position.x + (CELL_SIZE / 2), player.position.y + (CELL_SIZE / 2)},
offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2},
},
mode = .INTERACT,
speed = 1,
}
load_tilemap()
defer unload_tilemap()
fill_world_grid_with_nothing(&world)
place_random_trees(&world)
world = create_world("test_world", 10172020)
save_world(&world)
game_loop()
}
game_loop :: proc() {
pos_string : string
pos_cstring : cstring
for !rl.WindowShouldClose() {
@@ -50,7 +53,7 @@ game_loop :: proc() {
rl.BeginDrawing()
rl.ClearBackground(rl.BLACK)
rl.BeginMode2D(camera)
rl.BeginMode2D(player.camera)
draw()
@@ -59,25 +62,28 @@ game_loop :: proc() {
rl.DrawFPS(5,5)
player_grid_pos := get_player_grid_position(&player)
player_grid_pos_tile := get_grid_tile(&world, vec2_to_vec2i(player_grid_pos))
status_string := rl.TextFormat("POS: %v : %v | MODE: %v", player_grid_pos, player_grid_pos_tile.type, player.mode)
player_grid_pos_tile := get_world_tile(&world, vec2_to_vec2i(player_grid_pos))
current_chunk := get_chunk_from_world_pos(&world, player_grid_pos)
status_string := rl.TextFormat("POS: [%i,%i] : %v | Chunk: [%i,%i] : %v | MODE: %v", int(player_grid_pos.x), int(player_grid_pos.y), player_grid_pos_tile.type, current_chunk.position.x, current_chunk.position.y, get_biome_from_id(current_chunk.biome_id).name, player.mode)
pos_string := rl.TextFormat("Actual pos: %v", player.position)
rl.DrawText(status_string, 5, 25, 20, rl.RED)
// Debug: Draw collision check position
target_pos := player_grid_pos
chunk_pos := world_pos_to_chunk_pos(player_grid_pos)
local_pos := get_local_chunk_pos(vec2_to_vec2i(player_grid_pos))
format_string := rl.TextFormat("Grid: (%.0f,%.0f) Chunk: (%d,%d) Local: (%d,%d)",
player_grid_pos.x, player_grid_pos.y,
chunk_pos.x, chunk_pos.y,
local_pos.x, local_pos.y)
rl.DrawText(format_string, 10, 45, 20, rl.YELLOW)
rl.EndDrawing()
}
delete(pos_string)
delete(pos_cstring)
}
update :: proc() {
handle_player_input(&player, &world)
handle_window_resize()
camera.target = {player.position.x + (CELL_SIZE / 2), player.position.y + (CELL_SIZE / 2)}
player_update(&player, &world)
}
draw :: proc() {
@@ -85,16 +91,3 @@ draw :: proc() {
draw_player(&player)
}
handle_window_resize :: proc() {
if rl.IsWindowResized() {
camera.offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2}
}
}
print_grid :: proc() {
for x in 0..< len(world.grid) {
for y in 0..< len(world.grid) {
fmt.printfln("[%d, %d] %v", x, y, world.grid[x][y].type)
}
}
}

8
game/interactions.odin Normal file
View File

@@ -0,0 +1,8 @@
package game
import "core:fmt"
handle_tree_interaction :: proc(w:^World, p:^Player, pos:Vec2i) {
set_tile(w, nothing_tile, pos)
fmt.printfln("Collected Tree from %v", pos)
}

View File

@@ -1,8 +1,10 @@
package game
import "core:math"
Vec2i :: struct {
x: u32,
y:u32,
x: int,
y: int,
}
vec2i_to_vec2 :: proc(v2i:Vec2i) -> [2]f32 {
@@ -10,5 +12,15 @@ vec2i_to_vec2 :: proc(v2i:Vec2i) -> [2]f32 {
}
vec2_to_vec2i :: proc(v2:[2]f32) -> Vec2i {
return {u32(v2.x), u32(v2.y)}
return {int(math.floor(v2.x)), int(math.floor(v2.y))}
}
hash_noise :: proc(x, y: int, seed: i64) -> f32 {
h: i64 = i64(x) * 374761393
h *= i64(y) * 668265263
h *= seed
h *= 3266489917
h >>= 16
return f32(h & 0xFFFF) / 65535.0
}

View File

@@ -2,89 +2,226 @@ package game
import rl "vendor:raylib"
import "core:fmt"
import "core:math"
CHUNK_UNLOAD_DISTANCE :: 3
Player :: struct {
position : rl.Vector2,
move_timer: f32,
mode: InteractMode
mode: InteractMode,
camera: rl.Camera2D,
speed:f32
}
InteractMode :: enum {
INTERACT,
ATTACK,
STEAL,
}
handle_player_input :: proc(p : ^Player, w: ^World) {
@(private="file")
handle_player_camera :: proc(p:^Player) {
p.camera.target = {p.position.x + (CELL_SIZE / 2), p.position.y + (CELL_SIZE / 2)}
target_pos := get_player_grid_position(p)
if rl.IsWindowResized() {
p.camera.offset = {f32(rl.GetScreenWidth()) / 2, f32(rl.GetScreenHeight()) / 2}
}
}
player_update :: proc(p : ^Player, w: ^World) {
handle_player_input(p,w)
handle_player_camera(p)
// if rl.IsKeyPressed(.SPACE) {
// // set_tile(w, bricks_tile, vec2_to_vec2i(get_player_grid_position(p)))
// find_desert(w.seed)
// generate_biome_map(w.seed, 100, 100)
// }
}
@(private="file")
player_update_chunks :: proc(p: ^Player, w: ^World) {
// Configurable view distance (in chunks)
VIEW_DISTANCE :: 2
player_grid_pos := get_player_grid_position(p)
current_player_chunk := get_chunk_from_world_pos(w, player_grid_pos)
// Track which chunks should be loaded
chunks_to_keep := make(map[Vec2i]bool)
defer delete(chunks_to_keep)
// Load chunks in a square around the player's current chunk
for y := -VIEW_DISTANCE; y <= VIEW_DISTANCE; y += 1 {
for x := -VIEW_DISTANCE; x <= VIEW_DISTANCE; x += 1 {
chunk_pos := Vec2i{
current_player_chunk.position.x + x,
current_player_chunk.position.y + y,
}
// Load the chunk and mark it to keep
get_chunk(w, chunk_pos)
chunks_to_keep[chunk_pos] = true
}
}
// Unload chunks outside the view distance
for chunk_pos in w.chunks {
if !chunks_to_keep[chunk_pos] {
unload_chunk(chunk_pos, w)
}
}
}
@(private="file")
handle_player_input :: proc(p:^Player, w:^World) {
current_tile := get_world_tile(w, vec2_to_vec2i(get_player_grid_position(p)))
// Movement
dt := rl.GetFrameTime()
move_delay : f32 = 0.15
move_delay : f32 = 0.2 / p.speed
if p.move_timer > 0 {
p.move_timer -= dt
}
// fmt.printfln("MOVING TO: %v : %v", target_pos, get_grid_tile(w, vec2_to_vec2i(target_pos)).type)
if current_tile.type == .WATER {
p.speed = 0.3
}
else {
p.speed = 1
}
if p.move_timer <= 0 {
current_pos := get_player_grid_position(p)
if rl.IsKeyDown(.D) {
target_pos.x += 1
if !will_collide(target_pos, w) {
target_pos := rl.Vector2{current_pos.x + 1, current_pos.y}
if !will_collide(.RIGHT, p, w) {
player.position.x += CELL_SIZE
p.move_timer = move_delay
player_update_chunks(p,w)
}
}
if rl.IsKeyDown(.A) {
target_pos.x -= 1
if !will_collide(target_pos, w) {
target_pos := rl.Vector2{current_pos.x - 1, current_pos.y}
if !will_collide(.LEFT, p, w) {
player.position.x -= CELL_SIZE
p.move_timer = move_delay
}
player_update_chunks(p,w)
}
}
if rl.IsKeyDown(.W) {
target_pos.y -= 1
if !will_collide(target_pos, w) {
target_pos := rl.Vector2{current_pos.x, current_pos.y - 1}
if !will_collide(.UP, p, w) {
player.position.y -= CELL_SIZE
p.move_timer = move_delay
}
player_update_chunks(p,w)
}
}
if rl.IsKeyDown(.S) {
target_pos.y += 1
if !will_collide(target_pos, w) {
target_pos := rl.Vector2{current_pos.x, current_pos.y + 1}
if !will_collide(.DOWN, p, w) {
p.move_timer = move_delay
player.position.y += CELL_SIZE
}
player_update_chunks(p,w)
}
}
// Interactions
if rl.IsKeyPressed(.UP) {
if p.mode == .INTERACT { handle_interact(.UP, p, w) }
if p.mode == .ATTACK { handle_attack(.UP, p, w) }
}
if rl.IsKeyPressed(.DOWN) {
if p.mode == .INTERACT { handle_interact(.DOWN, p, w) }
if p.mode == .ATTACK { handle_attack(.DOWN, p, w) }
}
if rl.IsKeyPressed(.LEFT) {
if p.mode == .INTERACT { handle_interact(.LEFT, p, w) }
if p.mode == .ATTACK { handle_attack(.LEFT, p, w) }
}
if rl.IsKeyPressed(.RIGHT) {
if p.mode == .INTERACT { handle_interact(.RIGHT, p, w) }
if p.mode == .ATTACK { handle_attack(.RIGHT, p, w) }
}
if rl.IsKeyPressed(.ONE) { p.mode = .INTERACT }
if rl.IsKeyPressed(.TWO) { p.mode = .ATTACK }
if rl.IsKeyPressed(.THREE) { p.mode = .STEAL }
}
}
@(private="file")
InteractDirection :: enum {
UP, DOWN, LEFT, RIGHT
}
@(private="file")
handle_interact :: proc(direction:InteractDirection, p:^Player, w:^World) {
tile, pos := get_tile_in_direction(direction, p, w)
if tile.interaction == .RESOURCE && tile.resource != .NOTHING {
if tile.resource == .TREE { handle_tree_interaction(w, p, pos) }
}
}
@(private="file")
handle_attack :: proc(direction:InteractDirection, p:^Player, w:^World) {
tile, pos := get_tile_in_direction(direction, p, w)
}
@(private="file")
get_tile_in_direction :: proc(direction:InteractDirection, p:^Player, w:^World) -> (^Tile, Vec2i) {
grid_pos := get_player_grid_position(p)
if direction == .UP { grid_pos.y -= 1 }
if direction == .DOWN { grid_pos.y += 1 }
if direction == .LEFT { grid_pos.x -= 1 }
if direction == .RIGHT { grid_pos.x += 1 }
return get_world_tile(w, vec2_to_vec2i(grid_pos)), vec2_to_vec2i(grid_pos)
}
get_player_grid_position :: proc(player:^Player) -> rl.Vector2 {
grid_pos_x := player.position.x / CELL_SIZE
grid_pos_y := player.position.y / CELL_SIZE
grid_pos_x := math.floor(player.position.x / CELL_SIZE)
grid_pos_y := math.floor(player.position.y / CELL_SIZE)
return {grid_pos_x, grid_pos_y}
}
draw_player :: proc(player:^Player) {
draw_tile({27,0}, player.position, rl.DARKBLUE)
draw_tile({25,0}, player.position, {30,100,120,255})
// Debug: Draw player's grid cell
// player_grid_pos := get_player_grid_position(player)
// world_pos_x := player_grid_pos.x * CELL_SIZE
// world_pos_y := player_grid_pos.y * CELL_SIZE
// rl.DrawRectangleLines(
// i32(world_pos_x),
// i32(world_pos_y),
// i32(CELL_SIZE),
// i32(CELL_SIZE),
// rl.RED
// )
}
will_collide :: proc(pos:rl.Vector2, w:^World) -> bool {
if pos.y > WORLD_SIZE * CELL_SIZE || pos.x > WORLD_SIZE * CELL_SIZE {
return false
}
will_collide :: proc(direction:InteractDirection, p:^Player, w:^World) -> bool {
tile, pos := get_tile_in_direction(direction, p, w)
tile := get_grid_tile(w, vec2_to_vec2i(pos))
#partial switch tile.type {
case .WALL:
return true
}
if tile.type == .SOLID { return true }
return false
}

14
game/structures.odin Normal file
View File

@@ -0,0 +1,14 @@
package game
Structure :: struct {
name:string,
tile_map:[dynamic][dynamic]Tile,
// Other data here later like NPCs and enemies?
}
test_structure := Structure {
name = "Test",
tile_map = {
// Make a structure here?????
}
}

247
game/terrain.odin Normal file
View File

@@ -0,0 +1,247 @@
#+feature dynamic-literals
package game
import "core:fmt"
import "core:math"
import "core:math/noise"
BIOME_SCALE: f64 : 1
biome_list := map[u32]Biome {
0 = grasslands_biome,
1 = forest_biome,
2 = desert_biome,
3 = lake_biome,
}
BiomeType :: enum {
GRASSLAND,
FOREST,
LAKE,
DESERT,
}
Biome :: struct {
id: u32,
name: string,
type: BiomeType,
fauna_color: [4]u8,
valid_structures: [dynamic]u32,
}
// Define biome constants
grasslands_biome := Biome {
id = 0,
name = "Grasslands",
type = .GRASSLAND,
fauna_color = {50, 120, 25, 255},
valid_structures = {},
}
forest_biome := Biome {
id = 1,
name = "Forest",
type = .FOREST,
fauna_color = {30, 80, 20, 255},
valid_structures = {},
}
desert_biome := Biome {
id = 2,
name = "Desert",
type = .DESERT,
fauna_color = {200, 180, 100, 255},
valid_structures = {},
}
lake_biome := Biome {
id = 3,
name = "Lake",
type = .LAKE,
fauna_color = {0, 50, 150, 255},
valid_structures = {},
}
get_biome_from_id :: proc(id: u32) -> Biome {
return biome_list[id]
}
get_biome_type :: proc(world_pos: Vec2i, seed: i64) -> Biome {
// Use multiple noise scales for different features
continent_scale := 0.0008 // Very large scale features (continents)
region_scale := 0.007 // Medium scale features (regions)
local_scale := 0.025 // Local variations
// Use different seed offsets for each noise layer
continent_seed := seed
region_seed := seed + 10000
moisture_seed := seed + 20000
temperature_seed := seed + 30000
// Generate base continent shapes
continent := noise.noise_2d(
continent_seed,
{f64(world_pos.x) * continent_scale, f64(world_pos.y) * continent_scale},
)
// Amplify to get more defined continents
continent = math.pow(continent * 0.5 + 0.5, 1.5) * 2.0 - 1.0
// Generate regional variations
region := noise.noise_2d(
region_seed,
{f64(world_pos.x) * region_scale, f64(world_pos.y) * region_scale},
)
// Generate moisture and temperature maps for biome determination
moisture := noise.noise_2d(
moisture_seed,
{f64(world_pos.x) * region_scale, f64(world_pos.y) * region_scale},
)
temperature := noise.noise_2d(
temperature_seed,
{f64(world_pos.x) * region_scale, f64(world_pos.y) * region_scale},
)
// Adjust temperature to create larger hot regions
// This skews the distribution to have more areas with higher temperature
// temperature = math.pow(temperature * 0.5 + 0.5, 0.8) * 2.0 - 1.0
// Local variations (small details)
local_var :=
noise.noise_2d(seed, {f64(world_pos.x) * local_scale, f64(world_pos.y) * local_scale}) *
0.1
// Combine all factors with proper weighting
elevation := continent * 0.7 + region * 0.3 + local_var
// Convert noise values to 0-1 range for easier thresholding
normalized_elevation := elevation * 0.5 + 0.5
normalized_moisture := moisture * 0.5 + 0.5
normalized_temperature := temperature * 0.5 + 0.5
if normalized_elevation < 0.3 {
return lake_biome
}
if normalized_temperature > 0.7 && normalized_moisture < 0.2 {
return desert_biome
}
// Forests need moderate to high moisture
if normalized_moisture > 0.55 {
return forest_biome
}
// Default to grasslands
return grasslands_biome
}
// Improved chunk generation that considers neighboring chunks
generate_chunk :: proc(pos: Vec2i, seed: i64) -> Chunk {
chunk := Chunk {
position = pos,
}
// Store the biome for this chunk for consistency
chunk_center := Vec2i{pos.x * CHUNK_SIZE + CHUNK_SIZE / 2, pos.y * CHUNK_SIZE + CHUNK_SIZE / 2}
biome := get_biome_type(chunk_center, seed)
chunk.biome_id = biome.id
// Generate each tile, allowing for biome blending at edges
for x in 0 ..< CHUNK_SIZE {
for y in 0 ..< CHUNK_SIZE {
world_x := pos.x * CHUNK_SIZE + x
world_y := pos.y * CHUNK_SIZE + y
world_pos := Vec2i{world_x, world_y}
// Check the tile's specific biome (for transitions)
tile_biome := get_biome_type(world_pos, seed)
// Calculate distances to chunk edges for potential blending
edge_dist_x := min(x, CHUNK_SIZE - 1 - x)
edge_dist_y := min(y, CHUNK_SIZE - 1 - y)
edge_dist := min(edge_dist_x, edge_dist_y)
// Blend between chunk biome and tile biome near edges
// for smoother transitions between chunks
biome_to_use := biome
if edge_dist < 4 { // Within 4 tiles of chunk edge
blend_factor := f32(edge_dist) / 4.0
// Simple way to blend biomes - just pick one based on blend factor
// For a more sophisticated approach, you could actually blend features
if hash_noise(world_x, world_y, seed) > blend_factor {
biome_to_use = tile_biome
}
}
chunk.tiles[x][y] = generate_tile(world_pos, seed, biome_to_use)
}
}
return chunk
}
// Improved tile generation with biome transition support
generate_tile :: proc(pos: Vec2i, seed: i64, biome: Biome) -> Tile {
hash_value := hash_noise(pos.x, pos.y, seed)
// Use multiple noise scales for natural-looking features
large_scale := 0.025
medium_scale := 0.07
small_scale := 0.20
large_noise := noise.noise_2d(seed, {f64(pos.x) * large_scale, f64(pos.y) * large_scale})
medium_noise := noise.noise_2d(
seed + 5000,
{f64(pos.x) * medium_scale, f64(pos.y) * medium_scale},
)
small_noise := noise.noise_2d(
seed + 10000,
{f64(pos.x) * small_scale, f64(pos.y) * small_scale},
)
// Combine noise at different scales
combined_noise := large_noise * 0.6 + medium_noise * 0.3 + small_noise * 0.1
// Different biomes use the noise differently
switch biome.type {
case .GRASSLAND:
if combined_noise > 0.8 {
return tree_tile
} else if combined_noise > 0.2 {
return grass_tile
} else {
return nothing_tile
}
case .FOREST:
if combined_noise > 0.75 {
return double_tree_tile
} else if combined_noise > 0.4 {
return tree_tile
} else if combined_noise > 0.0 {
return grass_tile
} else {
return nothing_tile
}
case .DESERT:
cactus_noise := medium_noise * 0.5 + 0.5 // Normalize to 0-1
if cactus_noise > 0.8 && hash_value > 0.65 {
return cactus_tile
} else if combined_noise > 0.85 {
return dead_bush_tile
} else {
return nothing_tile
}
case .LAKE:
// Lakes can have different depths
if combined_noise > 0.7 {
return shallow_water_tile // You'd need to define this
} else {
return water_tile
}
case:
return nothing_tile
}
}

View File

@@ -3,21 +3,119 @@ package game
import rl "vendor:raylib"
import "core:math/rand"
tree_tile := Tile {
type = .WALL,
Tile :: struct #packed {
tilemap_pos: Vec2i,
color: [4]u8,
type: TileType,
interaction: InteractionType,
resource: ResourceType,
}
TileType :: enum u8 {
NOTHING,
SOLID,
FOLIAGE,
WATER,
}
ResourceType :: enum u8 {
NOTHING,
TREE,
BONE,
}
InteractionType :: enum u8 {
NOTHING,
RESOURCE,
ENEMY,
}
// Premade Tiles
nothing_tile := Tile { // The most common tile, makes up the majority of the world.
type = .NOTHING,
tilemap_pos = {0,0},
color = {0,0,0,255},
interaction = .NOTHING,
resource = .NOTHING
}
grass_tile := Tile { // Common fauna, more dense in grasslands
type = .FOLIAGE,
tilemap_pos = {5,0},
color = {50,120,25,255},
interaction = .NOTHING,
resource = .NOTHING
}
tree_tile := Tile { // Common grassland fauna, dense population in forests
type = .SOLID,
tilemap_pos = {0,1},
color = rl.DARKGREEN
color = {10,60,15,255},
resource = .TREE,
interaction = .RESOURCE,
}
place_random_trees :: proc(w:^World) {
for x in 0..< len(w.grid) {
for y in 0..< len(w.grid) {
chance := rand.int_max(100)
if chance <= 5 {
w.grid[x][y] = tree_tile
}
}
}
double_tree_tile := Tile { // Only found in forests, densly packed
type = .SOLID,
tilemap_pos = {3,2},
color = {10,60,15,255},
resource = .TREE,
interaction = .RESOURCE,
}
bricks_tile := Tile { // Unused, for now
type = .SOLID,
tilemap_pos = {10,17},
color = {140,30,10,255},
resource = .NOTHING,
interaction = .NOTHING,
}
water_tile := Tile { // Only seen in bodies of water
type = .WATER,
tilemap_pos = {19,1},
color = {5,10,70,255},
resource = .NOTHING,
interaction = .NOTHING,
}
shallow_water_tile := Tile { // Only seen in bodies of water
type = .WATER,
tilemap_pos = {19,1},
color = {5,40,80,255},
resource = .NOTHING,
interaction = .NOTHING,
}
cactus_tile := Tile { // Common desert fauna
type = .SOLID,
tilemap_pos = {6,1},
color = {5,40,0,255},
resource = .NOTHING,
interaction = .NOTHING,
}
double_cactus_tile := Tile { // Sparse desert fauna
type = .SOLID,
tilemap_pos = {7,1},
color = {5,40,0,255},
resource = .NOTHING,
interaction = .NOTHING,
}
cow_skull_tile := Tile { // Rare chance of spawning in a desert
type = .SOLID,
tilemap_pos = {1,15},
color = {200,200,200,255},
resource = .BONE,
interaction = .RESOURCE,
}
dead_bush_tile := Tile { // Common desert fauna
type = .FOLIAGE,
tilemap_pos = {6,2},
color = {145,100,30,255},
interaction = .NOTHING,
resource = .NOTHING
}

View File

@@ -2,60 +2,239 @@ package game
import rl "vendor:raylib"
import "core:fmt"
import "core:os"
import "core:path/filepath"
import "core:mem"
import "core:math"
CELL_SIZE :: 16
WORLD_SIZE :: 100
CHUNK_SIZE :: 32
WORLD_DATA_PATH :: "data/worlds"
World :: struct {
grid: [WORLD_SIZE][WORLD_SIZE]Tile
data_dir: string,
chunks: map[Vec2i]Chunk,
seed: i64
}
Tile :: struct {
type: TileType,
tilemap_pos:rl.Vector2,
color:rl.Color,
Chunk :: struct #packed {
position: Vec2i,
tiles: [CHUNK_SIZE][CHUNK_SIZE]Tile,
biome_id:u32,
}
TileType :: enum {
NOTHING,
WALL,
DOOR,
FLOOR,
}
set_grid_tile :: proc(w:^World, pos:Vec2i, t:Tile) {
w.grid[pos.x][pos.y] = t
}
get_grid_tile :: proc(w: ^World, pos: Vec2i) -> Tile {
if pos.x < 0 || pos.x >= len(w.grid) || pos.y < 0 || pos.y >= len(w.grid[0]) {
// fmt.printfln("Target [%v] outside of world bounds", pos)
return w.grid[0][0] // Default or error tile
create_world :: proc(name:string, seed:i64) -> World {
data_dir := fmt.tprintf("%v/%v", WORLD_DATA_PATH, name)
if !os.is_dir(data_dir) {
fmt.printfln("Data dir: %v does not exist", data_dir)
os.make_directory(data_dir)
}
chunk_dir := fmt.tprintf("%v/%v", data_dir, "chunks")
if !os.is_dir(chunk_dir) {
os.make_directory(chunk_dir)
}
return World {
data_dir = data_dir,
chunks = make(map[Vec2i]Chunk),
seed = seed
}
return w.grid[pos.x][pos.y]
}
fill_world_grid_with_nothing :: proc(w:^World) {
for x in 0..< len(w.grid) {
for y in 0..<len(w.grid) {
w.grid[x][y] = Tile {
type = .NOTHING,
tilemap_pos = {0,0}
}
load_world :: proc(name:string, seed:i64) -> World {
dir := fmt.tprintf("%v/%v", WORLD_DATA_PATH, name)
if !os.is_dir(dir) {
panic("Couldnt load world")
}
return World {
data_dir = dir,
chunks = make(map[Vec2i]Chunk),
seed = seed
}
}
save_world :: proc(w:^World) {
if !os.is_dir(w.data_dir) {
panic("World has invalid data_path")
}
// fmt.printfln("Saving world %v", w.data_dir)
for chunk in w.chunks {
save_chunk(get_chunk(w, chunk), w)
}
}
save_chunk :: proc(c:^Chunk, w:^World) {
chunk_dir := fmt.tprintf("%v/%v", w.data_dir, "chunks")
filename := fmt.tprintf("%v/%v_%v.chunk", chunk_dir, c.position.x, c.position.y)
// fmt.printfln("Saving chunk: %v", filename)
data := make([dynamic]u8)
// Append Position
for byte in transmute([size_of(int)]u8)c.position.x {append(&data, byte)}
for byte in transmute([size_of(int)]u8)c.position.y {append(&data, byte)}
// Append Tiles
for row in &c.tiles {
for tile in row {
for byte in transmute([size_of(int)]u8)tile.tilemap_pos.x {append(&data, byte)}
for byte in transmute([size_of(int)]u8)tile.tilemap_pos.y {append(&data, byte)}
for byte in transmute([4]u8)tile.color {append(&data, byte)}
for byte in transmute([size_of(TileType)]u8)tile.type {append(&data, byte)}
for byte in transmute([size_of(InteractionType)]u8)tile.interaction {append(&data, byte)}
for byte in transmute([size_of(ResourceType)]u8)tile.resource {append(&data, byte)}
}
}
// Biome ID
for byte in transmute([size_of(u32)]u8)c.biome_id {append(&data, byte)}
err := os.write_entire_file_or_err(filename, data[:])
}
load_chunk :: proc(pos:Vec2i, w:^World) -> Chunk {
chunk_dir := fmt.tprintf("%v/%v", w.data_dir, "chunks")
filename := fmt.tprintf("%v/%v_%v.chunk", chunk_dir, pos.x, pos.y)
data, err := os.read_entire_file_from_filename_or_err(filename)
if err != nil {
// fmt.printfln("No chunk %v found, generating new chunk", pos)
chunk := generate_chunk(pos, w.seed)
save_chunk(&chunk, w)
return chunk
}
chunk: Chunk
offset := 0
// Load Position
mem.copy(transmute([^]u8)&chunk.position.x, &data[offset], size_of(int))
offset += size_of(int)
mem.copy(transmute([^]u8)&chunk.position.y, &data[offset], size_of(int))
offset += size_of(int)
// Load tiles
for row_index := 0; row_index < len(chunk.tiles); row_index += 1 {
for tile_index := 0; tile_index < len(chunk.tiles[row_index]); tile_index += 1 {
tile := &chunk.tiles[row_index][tile_index] // Get address of tile
mem.copy(&tile.tilemap_pos.x, &data[offset], size_of(int))
offset += size_of(int)
mem.copy(&tile.tilemap_pos.y, &data[offset], size_of(int))
offset += size_of(int)
// Load color
color_temp: [4]u8
mem.copy(&color_temp, &data[offset], 4)
tile.color = color_temp
offset += 4
mem.copy(&tile.type, &data[offset], size_of(TileType))
offset += size_of(TileType)
mem.copy(&tile.interaction, &data[offset], size_of(InteractionType))
offset += size_of(InteractionType)
mem.copy(&tile.resource, &data[offset], size_of(ResourceType))
offset += size_of(ResourceType)
}
}
// Load Biome ID
mem.copy(transmute([^]u8)&chunk.biome_id, &data[offset], size_of(u32))
offset += size_of(u32)
return chunk
}
unload_chunk :: proc(pos:Vec2i, w:^World) {
_, exists := w.chunks[pos]
if exists {
save_chunk(get_chunk(w, pos), w)
delete_key(&w.chunks, pos)
}
}
get_chunk :: proc(w:^World, chunk_pos:Vec2i) -> ^Chunk {
chunk, exists := w.chunks[chunk_pos]
if !exists {
w.chunks[chunk_pos] = load_chunk(chunk_pos, w)
}
return &w.chunks[chunk_pos]
}
get_chunk_from_world_pos :: proc(w:^World, pos:rl.Vector2) -> ^Chunk {
chunk_pos := world_pos_to_chunk_pos(pos)
return get_chunk(w, chunk_pos)
}
world_pos_to_chunk_pos :: proc(pos:rl.Vector2) -> Vec2i {
x := int(math.floor(pos.x / CHUNK_SIZE))
y := int(math.floor(pos.y / CHUNK_SIZE))
return Vec2i{x,y}
}
get_local_chunk_pos :: proc(pos:Vec2i) -> Vec2i {
x := (pos.x % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE
y := (pos.y % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE
return Vec2i{x,y}
}
get_world_tile :: proc(w:^World, pos:Vec2i) -> ^Tile {
chunk_x := int(math.floor(f32(pos.x) / f32(CHUNK_SIZE)))
chunk_y := int(math.floor(f32(pos.y) / f32(CHUNK_SIZE)))
chunk_pos := Vec2i{chunk_x, chunk_y}
local_x := (pos.x % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE
local_y := (pos.y % CHUNK_SIZE + CHUNK_SIZE) % CHUNK_SIZE
local_pos := Vec2i{local_x, local_y}
chunk := get_chunk(w, chunk_pos)
return get_chunk_tile(chunk, local_pos)
// chunk_pos := world_pos_to_chunk_pos(vec2i_to_vec2(pos))
// local_pos := get_local_chunk_pos(pos)
//
// chunk := get_chunk(w, chunk_pos)
// return get_chunk_tile(chunk, local_pos)
}
get_chunk_tile :: proc(c:^Chunk, pos:Vec2i) -> ^Tile {
return &c.tiles[pos.x][pos.y]
}
set_chunk_tile :: proc(c:^Chunk, t:Tile, pos:Vec2i) {
c.tiles[pos.x][pos.y] = t
}
set_tile :: proc(w:^World, t:Tile, p:Vec2i) {
chunk := get_chunk_from_world_pos(w, vec2i_to_vec2(p))
set_chunk_tile(chunk, t, get_local_chunk_pos(p))
save_chunk(chunk, w)
}
draw_world :: proc(w:^World) {
for x in 0..< len(w.grid) {
for y in 0..< len(w.grid) {
tile := w.grid[x][y]
posX := x * TILE_SIZE
posY := y * TILE_SIZE
for chunk_pos, chunk in w.chunks {
for x in 0..<CHUNK_SIZE {
for y in 0..<CHUNK_SIZE {
tile := chunk.tiles[x][y]
world_x := chunk_pos.x * CHUNK_SIZE + x
world_y := chunk_pos.y * CHUNK_SIZE + y
pos := rl.Vector2{f32(world_x * CELL_SIZE), f32(world_y * CELL_SIZE)}
if tile.type != .NOTHING {
draw_tile(tile.tilemap_pos, {f32(posX), f32(posY)}, tile.color)
if tile.type != .NOTHING {
draw_tile(vec2i_to_vec2(tile.tilemap_pos), pos, rl.Color(tile.color))
}
}
}
}