commit 7206c1b038c8c33daac0e02655e9a450bf04eee1 Author: Kayne Ruse Date: Wed Jan 7 17:40:58 2026 +1100 Naive WFC kind of working diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/client.gd b/client.gd new file mode 100644 index 0000000..b172200 --- /dev/null +++ b/client.gd @@ -0,0 +1,63 @@ +extends Node + +#Master list of chunks +var _chunks: Array[Chunk] = [] + +#Create some test samples - these samples must ALWAYS be 9-elements long, 5th element is the result +var _samples: Array[PackedInt32Array] = [ + [1,1,1, 1,1,1, 1,1,1], +] + +@onready +var wfc: Node = get_node("../Generator") + +func _ready() -> void: + var ruleset: PackedInt32Array = read_sample_ruleset("sample1.png") + var samples: Array[PackedInt32Array] = parse_samples_from_ruleset(ruleset) + + #print(ruleset) + #print(samples) + wfc.generate_chunk_at(0,0,_chunks,samples) + +## Read the png file, and parse it to a useable ruleset +func read_sample_ruleset(filename: String) -> PackedInt32Array: + var img: Image = Image.load_from_file(filename) + var png: PackedByteArray = img.get_data() + var hex: PackedInt32Array = [] + + @warning_ignore("integer_division") + var size: int = (png.size() / 3) + hex.resize(size) + + print(png) + for i in range(size): #the file is assumed to be in RGB format + hex[i] = (png[i * 3] << 16) | (png[i * 3 + 1] << 8) | (png[i * 3 + 2] << 0) + print(i, "(", hex[i], "): ", png[i * 3], ",", png[i * 3 + 1], ",", png[i * 3 + 2]) + return hex + +func parse_samples_from_ruleset(ruleset: PackedInt32Array) -> Array[PackedInt32Array]: + #for now, assume the ruleset is 8x8 + const RULESET_WIDTH: int = 8 + const RULESET_HEIGHT: int = 8 + + var samples: Array[PackedInt32Array] = [] + + for x in range(1, RULESET_WIDTH-1): + for y in range(1, RULESET_HEIGHT-1): + var sample: PackedInt32Array = [ + ruleset[(y -1) * RULESET_WIDTH + (x -1)], + ruleset[(y -1) * RULESET_WIDTH + (x )], + ruleset[(y -1) * RULESET_WIDTH + (x +1)], + + ruleset[(y ) * RULESET_WIDTH + (x -1)], + ruleset[(y ) * RULESET_WIDTH + (x )], + ruleset[(y ) * RULESET_WIDTH + (x +1)], + + ruleset[(y +1) * RULESET_WIDTH + (x -1)], + ruleset[(y +1) * RULESET_WIDTH + (x )], + ruleset[(y +1) * RULESET_WIDTH + (x +1)], + ] + + samples.append(sample) + + return samples diff --git a/client.gd.uid b/client.gd.uid new file mode 100644 index 0000000..6eae30c --- /dev/null +++ b/client.gd.uid @@ -0,0 +1 @@ +uid://jmttidfcknea diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..fa6d245 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ba3kljgd5yvc2" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..22939e0 --- /dev/null +++ b/project.godot @@ -0,0 +1,20 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Wave" +run/main_scene="uid://d2g4ooi0x6ebo" +config/features=PackedStringArray("4.5", "GL Compatibility") +config/icon="res://icon.svg" + +[rendering] + +renderer/rendering_method="mobile" diff --git a/sample1.png b/sample1.png new file mode 100644 index 0000000..598a7bf Binary files /dev/null and b/sample1.png differ diff --git a/sample1.png.import b/sample1.png.import new file mode 100644 index 0000000..1c1fcb4 --- /dev/null +++ b/sample1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dy1xsv1acvc4i" +path="res://.godot/imported/sample1.png-8b828765c2f6336dd2e8e4a034a21542.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://sample1.png" +dest_files=["res://.godot/imported/sample1.png-8b828765c2f6336dd2e8e4a034a21542.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scene.tscn b/scene.tscn new file mode 100644 index 0000000..93fc410 --- /dev/null +++ b/scene.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=3 format=3 uid="uid://d2g4ooi0x6ebo"] + +[ext_resource type="Script" uid="uid://bf6phxhnvh0ul" path="res://wfc/generator.gd" id="1_ulcgi"] +[ext_resource type="Script" uid="uid://jmttidfcknea" path="res://client.gd" id="2_nxogm"] + +[node name="Scene" type="Node2D"] + +[node name="Generator" type="Node" parent="."] +script = ExtResource("1_ulcgi") + +[node name="Client" type="Node" parent="."] +script = ExtResource("2_nxogm") diff --git a/wfc/chunk.gd b/wfc/chunk.gd new file mode 100644 index 0000000..183dadd --- /dev/null +++ b/wfc/chunk.gd @@ -0,0 +1,31 @@ +class_name Chunk extends Object +## A Chunk of map data and bundled metadata, always aligned to CHUNK_WIDTH and CHUNK_HEIGHT +## The default data values are 0, which means "NOT SET" +## Chunks can be partially unset, to allow for prefabs +## Any values of -1 are "NOT VALID" + +const CHUNK_WIDTH: int = 16 +const CHUNK_HEIGHT: int = 16 + +var data: PackedInt32Array +var x: int +var y: int + +func _init(_x: int, _y: int): + assert(_x % CHUNK_WIDTH == 0) + assert(_y % CHUNK_HEIGHT == 0) + x = _x + y = _y + data = PackedInt32Array() + data.resize(CHUNK_WIDTH * CHUNK_HEIGHT) + data.fill(0) + +func get_tile(tile_x: int, tile_y: int) -> int: + if tile_x < 0 or tile_y < 0 or tile_x >= CHUNK_WIDTH or tile_y >= CHUNK_HEIGHT: return -1 + return data[tile_y * CHUNK_WIDTH + tile_x] + +func set_tile(tile_x: int, tile_y: int, value: int) -> int: + assert(value > 0) #do NOT clear a tile with this function + if tile_x < 0 or tile_y < 0 or tile_x >= CHUNK_WIDTH or tile_y >= CHUNK_HEIGHT: return -1 + data[tile_y * CHUNK_WIDTH + tile_x] = value + return value diff --git a/wfc/chunk.gd.uid b/wfc/chunk.gd.uid new file mode 100644 index 0000000..c9f2115 --- /dev/null +++ b/wfc/chunk.gd.uid @@ -0,0 +1 @@ +uid://due7w7hripbuk diff --git a/wfc/generator.gd b/wfc/generator.gd new file mode 100644 index 0000000..0a68ba3 --- /dev/null +++ b/wfc/generator.gd @@ -0,0 +1,52 @@ +extends Node +## The generator works with pure numbers, you'll need something else to convert it to actual tiles + +## Makes a new chunk, derived from the given WFC samples +func generate_chunk_at(_x: int, _y: int, _chunk_array: Array[Chunk], _samples: Array[PackedInt32Array]) -> Chunk: + #TODO: fix the chunk-edges + var chunk: Chunk = Chunk.new(_x, _y) + + var latch: int = 1 + while latch: + latch = 0 #if set to true, the generator needs another pass + #iterate over unset tiles with valid neighbours + for x in range(Chunk.CHUNK_WIDTH): + for y in range((Chunk.CHUNK_HEIGHT)): + if chunk.data[y * Chunk.CHUNK_HEIGHT + x] == 0: #direct access, to skip extra checks + #TODO: convert this to the low-entropy method + if _set_tile_from_samples(chunk, x, y, _samples) > 0: + latch += 1 + print("latch: ", latch) + + print(chunk.data) + _chunk_array.append(chunk) + return chunk + +## Returns the value set, or -1 if no options found +func _set_tile_from_samples(chunk: Chunk, tile_x: int, tile_y: int, _samples: Array[PackedInt32Array]) -> int: + var valid: Array[PackedInt32Array] = [] + + #use a lambda for easy reading below + var compare := func (tile_value: int, sample: int) -> bool: + return tile_value <= 0 or tile_value == sample + + #find all valid samples + for sample in _samples: + if !compare.call(chunk.get_tile(tile_x -1, tile_y -1), sample[0]): continue + if !compare.call(chunk.get_tile(tile_x , tile_y -1), sample[1]): continue + if !compare.call(chunk.get_tile(tile_x +1, tile_y -1), sample[2]): continue + if !compare.call(chunk.get_tile(tile_x -1, tile_y ), sample[3]): continue + # // + if !compare.call(chunk.get_tile(tile_x +1, tile_y ), sample[5]): continue + if !compare.call(chunk.get_tile(tile_x -1, tile_y +1), sample[6]): continue + if !compare.call(chunk.get_tile(tile_x , tile_y +1), sample[7]): continue + if !compare.call(chunk.get_tile(tile_x +1, tile_y +1), sample[8]): continue + + valid.append(sample) + print("Valid samples: ", valid.size()) + if valid.size() <= 0: return -1 + var s = valid.pick_random() + + #set the tile to the randomly selected value + chunk.set_tile(tile_x, tile_y, s[4]) + return s[4] diff --git a/wfc/generator.gd.uid b/wfc/generator.gd.uid new file mode 100644 index 0000000..c60c4a2 --- /dev/null +++ b/wfc/generator.gd.uid @@ -0,0 +1 @@ +uid://bf6phxhnvh0ul