diff --git a/assets/cactus.png b/assets/cactus.png new file mode 100644 index 0000000..b53bcbb Binary files /dev/null and b/assets/cactus.png differ diff --git a/assets/dirt.png b/assets/dirt.png new file mode 100644 index 0000000..525d9ef Binary files /dev/null and b/assets/dirt.png differ diff --git a/assets/flowers.png b/assets/flowers.png new file mode 100644 index 0000000..57efbec Binary files /dev/null and b/assets/flowers.png differ diff --git a/assets/grass.png b/assets/grass.png new file mode 100644 index 0000000..5484f54 Binary files /dev/null and b/assets/grass.png differ diff --git a/assets/haunted.png b/assets/haunted.png new file mode 100644 index 0000000..2e05b72 Binary files /dev/null and b/assets/haunted.png differ diff --git a/assets/rocks.png b/assets/rocks.png new file mode 100644 index 0000000..e4d7d4e Binary files /dev/null and b/assets/rocks.png differ diff --git a/assets/sand.png b/assets/sand.png new file mode 100644 index 0000000..464f638 Binary files /dev/null and b/assets/sand.png differ diff --git a/assets/stone.png b/assets/stone.png new file mode 100644 index 0000000..386ae97 Binary files /dev/null and b/assets/stone.png differ diff --git a/assets/tiles.png b/assets/tiles.png new file mode 100644 index 0000000..081fc9b Binary files /dev/null and b/assets/tiles.png differ diff --git a/assets/tree.png b/assets/tree.png new file mode 100644 index 0000000..cf6ebcb Binary files /dev/null and b/assets/tree.png differ diff --git a/assets/tree_pine.png b/assets/tree_pine.png new file mode 100644 index 0000000..239edb9 Binary files /dev/null and b/assets/tree_pine.png differ diff --git a/assets/water.png b/assets/water.png new file mode 100644 index 0000000..2a296bf Binary files /dev/null and b/assets/water.png differ diff --git a/src/main.cr b/src/main.cr index ca0e4a8..a9db35f 100644 --- a/src/main.cr +++ b/src/main.cr @@ -1,167 +1,211 @@ -require "sdl" -require "sdl/image" +require "json" -## Helper code comes here +require "./naka.cr" -class Naka - def self.init - SDL.init SDL::Init::VIDEO - SDL::IMG.init SDL::IMG::Init::PNG +class Map + @save_directory : String + getter altitude_maps : Naka::Noise2D + getter humidity_maps : Naka::Noise2D + getter evil_maps : Naka::Noise2D + getter flowers_map : Naka::Noise2D + def initialize(@save_directory) + Dir.mkdir_p @save_directory - at_exit { SDL.quit } - end -end + @altitude_maps = Naka::Noise2D.new.tap do |a| + a.add_octave(32, 1) + a.add_octave(16, 0.5) + a.add_octave(8, 0.25) + a.add_octave(4, 0.125) + a.add_octave(2, 1/16) # wtf, too smol + end -class Naka::Renderer < SDL::Renderer -end + @humidity_maps = Naka::Noise2D.new.tap do |a| + a.add_octave(64, 1) + a.add_octave(32, 0.5) + a.add_octave(16, 0.25) + a.add_octave(8, 0.20) + a.add_octave(4, 0.15) + a.add_octave(2, 0.10) + end -class Naka::Window - alias Flags = LibSDL::WindowFlags - alias Position = LibSDL::WindowPosition + @evil_maps = Naka::Noise2D.new.tap do |a| + a.add_octave(48, 0.6) + a.add_octave(32, 0.6) + a.add_octave(16, 0.3) + end - getter renderer - - def initialize(title, width, height, - x : Position = Position::UNDEFINED, - y : Position = Position::UNDEFINED, - flags : Flags = Flags::SHOWN) - - @window = SDL::Window.new(title, width, height, x, y, flags) - @renderer = Naka::Renderer.new @window + @flowers_map = Naka::Noise2D.new(1, 1) end - def newImage(file_path) : SDL::Texture - SDL::IMG.load file_path, @renderer - end + class Object + getter type : String + def initialize(@type) + end - # FIXME: We’ll probably want options for scaling, rotations, and so on. - def draw(texture : SDL::Texture, x : Int32, y : Int32) - @renderer.copy texture, - dstrect: SDL::Rect[x, y, texture.width, texture.height] - end -end - -class Naka::Timer - getter start - getter last_update - - def initialize - @start = Time.now - @last_update = @start - end - - def step - now = Time.now - - dt = now.epoch_ms - @last_update.epoch_ms - - @last_update = now - - dt - end -end - -class Naka::Event - class Quit - end - - class Draw - getter window : Naka::Window - - def initialize(@window) + def to_json(builder) + builder.object do + builder.field "type", @type + end end end - class Update - getter dt : Int64 + class Tile < Array(Object) + def initialize(element) + initialize - def initialize(@dt) + self << element end end - # FIXME: Split in KeyUp and KeyDown. - class KeyUp - getter key : LibSDL::Keycode - getter scan_code : LibSDL::Scancode + CHUNK_SIZE = 32 + class Chunk + property tiles : Array(Array(Tile)) + getter x : Int32 + getter y : Int32 - def initialize(@key, @scan_code) - end - end - class KeyDown - getter key : LibSDL::Keycode - getter scan_code : LibSDL::Scancode - getter repeat : Bool + def initialize(@x, @y, map) + p "Creating Map::Chunk" - def initialize(@key, @scan_code, @repeat) - end - end + @tiles = Array.new(CHUNK_SIZE) do |x| + x += @x * CHUNK_SIZE - # FIXME: THIS MAIN LOOP IS **NOT** READY FOR PRODUCTION - # Update events are still missing, and both Draw and Update should - # be time-based. - # Why having a `window` parameter instead of making a Window#loop method? - # Because we may want to have a `windows` parameter in the future, although - # unlikely. But still possible. - def self.loop(window, &block) - max_fps = 60 - max_dt = 1000. / 60 + Array.new(CHUNK_SIZE) do |y| + y += @y * CHUNK_SIZE - timer = Naka::Timer.new + Tile.new(Object.new "stone").tap do |i| + altitude = map.altitude_maps.get(x, y) + 0.5 - renderer = window.renderer + humidity = map.humidity_maps.get(x, y) + 0.5 - ::loop do - exit_requested = false + evil = map.evil_maps.get(x, y) - while event = SDL::Event.poll - r_value = yield case event - when SDL::Event::Quit - Naka::Event::Quit.new - when SDL::Event::Keyboard - keysym = event.keysym - if event.type == LibSDL::EventType::KEYUP - Naka::Event::KeyUp.new keysym.sym, keysym.scancode - elsif event.type == LibSDL::EventType::KEYDOWN - Naka::Event::KeyDown.new keysym.sym, keysym.scancode, event.repeat != 0 + if altitude < 0.25 + i << Object.new "sand" + + if altitude < 0.175 + i << Object.new "water" + else + if humidity < 0.33 + i << Object.new "sand" + else + i << Object.new "dirt" + end + end + elsif altitude < 0.75 + i << Object.new "dirt" + + if evil > 0.5 + i << Object.new "haunted" + else + i << Object.new "grass" + end + + if altitude > 0.35 && altitude < 0.65 + if humidity > 0.4 + if humidity > 0.8 + i << Object.new "tree" + elsif humidity < 0.6 + i << Object.new "pine" + else + if Random.rand(2) == 0 + i << Object.new "tree" + else + i << Object.new "pine" + end + end + else + if Random.rand(10) == 0 + i << Object.new "flowers" + end + end + end + elsif altitude > 0.85 + i << Object.new "rocks" + end end - else # FIXME: Most event types may need proper Naka:: classes. - event - end - - if r_value == Naka::Event::Quit - exit_requested = true - break end end - - break if exit_requested - - dt = timer.step - - r_value = yield Naka::Event::Update.new dt - - break if r_value == Naka::Event::Quit - - renderer.draw_color = SDL::Color[255, 255, 255, 255] - renderer.clear - - yield Naka::Event::Draw.new window - - renderer.present - - sleep 0.001 end + + def to_json + { + :x => @x, + :y => @y, + :tiles => @tiles + }.to_json + end + + def get(x, y) + @tiles[x][y] + end + end + + @chunks = Hash(Int32, Hash(Int32, Chunk)).new + + def get_chunk(x, y) + chunks_list = @chunks[x]? + if chunks_list.nil? + chunks_list = Hash(Int32, Chunk).new + @chunks[x] = chunks_list + end + + chunk = chunks_list[y]? + if chunk.nil? + chunk = Chunk.new x, y, self + chunks_list[y] = chunk + + save chunk + end + + chunk + end + + def save(chunk : Chunk) + File.write "#{@save_directory}/chunk-#{chunk.x}-#{chunk.y}.json", chunk.to_json + end + + def get(x, y) + get_chunk(x / CHUNK_SIZE, y / CHUNK_SIZE).get(x % CHUNK_SIZE, y % CHUNK_SIZE) end end -## Actual application code comes here +class ImagesLoader + getter grass : SDL::Texture + getter stone : SDL::Texture + getter dirt : SDL::Texture + getter tree : SDL::Texture + getter pine : SDL::Texture + getter flowers : SDL::Texture + getter sand : SDL::Texture + getter water : SDL::Texture + getter haunted : SDL::Texture + getter rocks : SDL::Texture + + def initialize(window) + @grass = window.newImage "assets/grass.png" + @stone = window.newImage "assets/stone.png" + @dirt = window.newImage "assets/dirt.png" + @tree = window.newImage "assets/tree.png" + @flowers = window.newImage "assets/flowers.png" + @sand = window.newImage "assets/sand.png" + @water = window.newImage "assets/water.png" + @haunted = window.newImage "assets/haunted.png" + @pine = window.newImage "assets/tree_pine.png" + @rocks = window.newImage "assets/rocks.png" + end +end Naka.init -window = Naka::Window.new "Test", 640, 480 +window = Naka::Window.new "Test", 16*128, 16*128, flags: LibSDL::WindowFlags::RESIZABLE | LibSDL::WindowFlags::SHOWN | LibSDL::WindowFlags::OPENGL -naka = window.newImage "naka.png" +images = ImagesLoader.new window + +connection_font = window.newFont "Connection.otf", 30.5 +window.set_font connection_font + +map = Map.new "map-test-01" Naka::Event.loop window do |event| case event @@ -169,7 +213,54 @@ Naka::Event.loop window do |event| next Naka::Event::Quit when Naka::Event::Update when Naka::Event::Draw - event.window.draw naka, 20, 20 + zoom_level = 1.0 + + 128.times do |x| + 128.times do |y| + tile = map.get x, y + + # FIXME: + # - Please don’t draw more than one layer of ground… + # - Transitions. (eg. between dirt and grass) + tile.each do |object| + image = case object.type + when "grass" + images.grass + when "dirt" + images.dirt + when "stone" + images.stone + when "tree" + images.tree + when "pine" + images.pine + when "flowers" + images.flowers + when "water" + images.water + when "haunted" + images.haunted + when "sand" + images.sand + when "rocks" + images.rocks + else + images.stone + end + + window.draw( + image, + x: (x * 16 * zoom_level).to_i, + y: (y * 16 * zoom_level - (image.height - 16) * zoom_level).to_i, + scale_x: zoom_level, + scale_y: zoom_level + ) + end + end + end + + # Urgh. This shit eats CPU. Fonts need goddamn fixing. + #window.print "The quick brown fox jumps over the lazy dog.", 50, 50 when Naka::Event::KeyUp, Naka::Event::KeyDown pp! event end diff --git a/src/naka.cr b/src/naka.cr new file mode 100644 index 0000000..febfb97 --- /dev/null +++ b/src/naka.cr @@ -0,0 +1,203 @@ + +require "sdl" +require "sdl/image" +require "sdl/ttf" + +class Naka +end + +require "./noise.cr" + +## Helper code comes here + +class Naka + def self.init + SDL.init SDL::Init::VIDEO + SDL::IMG.init SDL::IMG::Init::PNG + SDL::TTF.init + + at_exit { + SDL.quit + SDL::TTF.quit + } + end +end + +class Naka::Renderer < SDL::Renderer +end + +class Naka::Window + alias Flags = LibSDL::WindowFlags + alias Position = LibSDL::WindowPosition + + getter renderer + + @current_font : SDL::TTF::Font? = nil + + def initialize(title, width, height, + x : Position = Position::UNDEFINED, + y : Position = Position::UNDEFINED, + flags : Flags = Flags::SHOWN) + + @window = SDL::Window.new(title, width, height, x, y, flags) + @renderer = Naka::Renderer.new @window + + @renderer.draw_blend_mode = SDL::BlendMode::BLEND + end + + def newImage(file_path) : SDL::Texture + SDL::IMG.load(file_path, @renderer).tap do |texture| + texture.blend_mode = SDL::BlendMode::BLEND + end + end + + def newFont(file_path, font_size = 12) : SDL::TTF::Font + SDL::TTF::Font.new file_path, font_size + end + + # FIXME: We’ll probably want options for scaling, rotations, and so on. + def draw(texture : SDL::Texture, x : Int32, y : Int32, scale_x = 1, scale_y = 1) + @renderer.copy texture, + dstrect: SDL::Rect[x, y, (texture.width * scale_x).to_i, (texture.height * scale_y).to_i] + end + + def draw(surface : SDL::Surface, x : Int32, y : Int32) + @renderer.copy surface, + dstrect: SDL::Rect[x, y, surface.width, surface.height] + end + + def set_font(@current_font) + end + + # FIXME: + # - This only prints properly “solid” text. Shaded text (depixelated) + # would need some additional OpenGL manipulations. + # - This is very inefficient resources-wise. If we are willing to drop + # ligatures support (is it even supported in SDL::TTF?), we should + # build a textures atlas of all glyphs and use that to render text. + # - Alternatively, it may be worth it to let users generate a texture of + # some text and draw it, manually. + def print(text, x, y) + font = @current_font + + return if font.nil? + + surface = font.render_solid(text, SDL::Color[0, 0, 0, 255], @renderer.draw_color) + + draw surface, x, y + end +end + +class Naka::Timer + getter start + getter last_update + + def initialize + @start = Time.now + @last_update = @start + end + + def step + now = Time.now + + dt = now.epoch_ms - @last_update.epoch_ms + + @last_update = now + + dt + end +end + +class Naka::Event + class Quit + end + + class Draw + getter window : Naka::Window + + def initialize(@window) + end + end + + class Update + getter dt : Int64 + + def initialize(@dt) + end + end + + # FIXME: Split in KeyUp and KeyDown. + class KeyUp + getter key : LibSDL::Keycode + getter scan_code : LibSDL::Scancode + + def initialize(@key, @scan_code) + end + end + class KeyDown + getter key : LibSDL::Keycode + getter scan_code : LibSDL::Scancode + getter repeat : Bool + + def initialize(@key, @scan_code, @repeat) + end + end + + # FIXME: THIS MAIN LOOP IS **NOT** READY FOR PRODUCTION + # Update events are still missing, and both Draw and Update should + # be time-based. + # Why having a `window` parameter instead of making a Window#loop method? + # Because we may want to have a `windows` parameter in the future, although + # unlikely. But still possible. + def self.loop(window, &block) + max_fps = 60 + max_dt = 1000. / 60 + + timer = Naka::Timer.new + + renderer = window.renderer + + ::loop do + exit_requested = false + + while event = SDL::Event.poll + r_value = yield case event + when SDL::Event::Quit + Naka::Event::Quit.new + when SDL::Event::Keyboard + keysym = event.keysym + if event.type == LibSDL::EventType::KEYUP + Naka::Event::KeyUp.new keysym.sym, keysym.scancode + elsif event.type == LibSDL::EventType::KEYDOWN + Naka::Event::KeyDown.new keysym.sym, keysym.scancode, event.repeat != 0 + end + else # FIXME: Most event types may need proper Naka:: classes. + event + end + + if r_value == Naka::Event::Quit + exit_requested = true + break + end + end + + break if exit_requested + + dt = timer.step + + r_value = yield Naka::Event::Update.new dt + + break if r_value == Naka::Event::Quit + + renderer.draw_color = SDL::Color[255, 255, 255, 255] + renderer.clear + + yield Naka::Event::Draw.new window + + renderer.present + + sleep 0.001 + end + end +end + diff --git a/src/noise.cr b/src/noise.cr new file mode 100644 index 0000000..9bebbd4 --- /dev/null +++ b/src/noise.cr @@ -0,0 +1,105 @@ + +# FIXME: We may want to generate noise of other numbers of dimensions. +class Naka::Noise2D + class Octave + record Vec2, x : Float64, y : Float64 + + private def lerp(a, b, v) + a * (1.0 - v) + b * v + end + + private def smooth(v) + v * v * (3.0 - 2.0 * v) + end + + private def gradient(orig, grad, p) + sp = Vec2.new(p.x - orig.x, p.y - orig.y) + grad.x * sp.x + grad.y * sp.y + end + + private def random_gradient + v = rand * Math::PI * 2.0 + Vec2.new(Math.cos(v), Math.sin(v)) + end + + @frequency : Float64 | Int32 + @amplitude : Float64 | Int32 + + def amplitude + @amplitude.to_f + end + def frequency + @frequency.to_f + end + + def initialize(@frequency, @amplitude) + @rgradients = StaticArray(Vec2, 256).new { random_gradient } + @permutations = StaticArray(Int32, 256).new { |i| i } + @permutations.shuffle! + end + + def get_gradient(x, y) + idx = @permutations[x & 255] + @permutations[y & 255] + @rgradients[idx & 255] + end + + def get_gradients(x, y) + x0f = x.floor + y0f = y.floor + x0 = x0f.to_i + y0 = y0f.to_i + x1 = x0 + 1 + y1 = y0 + 1 + + { + { + get_gradient(x0, y0), + get_gradient(x1, y0), + get_gradient(x0, y1), + get_gradient(x1, y1), + }, + { + Vec2.new(x0f + 0.0, y0f + 0.0), + Vec2.new(x0f + 1.0, y0f + 0.0), + Vec2.new(x0f + 0.0, y0f + 1.0), + Vec2.new(x0f + 1.0, y0f + 1.0), + }, + } + end + + def get(x, y) + x = x.to_f / @frequency + y = y.to_f / @frequency + + p = Vec2.new(x, y) + gradients, origins = get_gradients(x, y) + v0 = gradient(origins[0], gradients[0], p) + v1 = gradient(origins[1], gradients[1], p) + v2 = gradient(origins[2], gradients[2], p) + v3 = gradient(origins[3], gradients[3], p) + fx = smooth(x - origins[0].x) + vx0 = lerp(v0, v1, fx) + vx1 = lerp(v2, v3, fx) + fy = smooth(y - origins[0].y) + + lerp(vx0, vx1, fy) * @amplitude.to_f + end + end + + + @storage = Array(Octave).new + def initialize + end + def initialize(a, f) + add_octave a, f + end + + def add_octave(a, f) + @storage << Octave.new a, f + end + + def get(x, y) + @storage.reduce 0 { |a, e| a + e.get(x, y) } + end +end +