Compare commits

...

11 Commits

Author SHA1 Message Date
1fe75a871e first house 2025-10-21 01:30:08 +02:00
191235da54 working keyboard and plugin for better tilemaps 2025-10-17 01:00:19 +02:00
69c3e6f396 event system and keyboard 2025-10-16 00:54:49 +02:00
f87cc28c57 fixed saving & added first basic save creation menu 2025-10-15 05:36:33 +02:00
283e3cbb5a rendering fixes and menu 2025-10-15 00:18:23 +02:00
ea63279633 save management 2025-10-14 20:19:29 +02:00
21a25c07a8 improved limits calculation 2025-10-14 02:34:41 +02:00
6d3652aa84 limits and physics 2025-10-14 01:58:28 +02:00
bdaf2901db world tilemap 2025-10-12 22:19:01 +02:00
9dc7f22123 Merge branch 'main' of git.vaporvee.com:vaporvee/project-hood 2025-10-11 01:10:17 +02:00
b44ff2b09d player spritesheet 2025-10-11 01:08:31 +02:00
88 changed files with 3624 additions and 24 deletions

View File

@@ -0,0 +1,70 @@
##[br] Watches a TileSetAtlasSource for changes.
##[br] Causes its 'parent' TileSetWatcher to emit terrains_changed when the atlas changes.
##[br] Also emits parent.atlas_autotiled when it thinks the user auto-generated atlas tiles.
class_name AtlasWatcher
## Prevents the number of seen atlases from extending to infinity.
const UNDO_LIMIT = 1024
## Stores all of the atlas instance id's that have been seen before, to prevent autogen on redo.
static var _registered_atlases := []
## The TileSetWatcher that created this AtlasWatcher. Used to send signals back.
var parent: TileSetWatcher
## The Source ID of `self.atlas`.
var sid: int
## The atlas to be watched for changes.
var atlas: TileSetAtlasSource
func _init(parent: TileSetWatcher, sid: int, atlas: TileSetAtlasSource) -> void:
self.parent = parent
self.sid = sid
self.atlas = atlas
atlas.changed.connect(_atlas_changed, ConnectFlags.CONNECT_DEFERRED)
var id := atlas.get_instance_id()
# should not autogen if atlas was created through redo, i.e. its instance id already existed
if _atlas_is_empty() and id not in _registered_atlases:
_registered_atlases.push_back(id)
if _registered_atlases.size() > UNDO_LIMIT:
_registered_atlases.pop_front()
atlas.changed.connect(_detect_autogen, ConnectFlags.CONNECT_DEFERRED | ConnectFlags.CONNECT_ONE_SHOT)
func _atlas_is_empty() -> bool:
return atlas.get_tiles_count() == 0
## Returns true if the texture has any opaque pixels in the specified tile coordinates.
func _is_opaque_tile(image: Image, tile: Vector2i, p_threshold: float = 0.1) -> bool:
# We cannot use atlas.get_tile_texture_region(tile) as it fails on unregistered tiles.
var region := Rect2i(tile * atlas.texture_region_size, atlas.texture_region_size)
var sprite := image.get_region(region)
if sprite.is_invisible():
return false
# We're still not sure if the tile is empty or not.
# Godot's auto-gen considers 0.1 opacity as "transparent" but not "invisible".
for y in range(region.position.y, region.end.y):
for x in range(region.position.x, region.end.x):
if image.get_pixel(x, y).a > p_threshold:
return true
return false
##[br] HACK: literally just tries to guess which tiles the terrain autogen system would make
##[br] Called once, and only once, at the end of the first frame that a texture is created.
func _detect_autogen() -> void:
var size := Vector2i(atlas.texture.get_size()) / atlas.texture_region_size
var image := atlas.texture.get_image()
var expected_tiles := []
for y in size.y:
for x in size.x:
var tile := Vector2i(x, y)
if atlas.has_tile(tile) != _is_opaque_tile(image, tile):
return
parent.atlas_autotiled.emit(sid, atlas)
## Called every time the atlas changes. Simply flags that terrains have changed.
func _atlas_changed() -> void:
parent._flag_terrains_changed = true

View File

@@ -0,0 +1 @@
uid://cfhiw77a85x8w

View File

@@ -0,0 +1,41 @@
@icon('CursorDual.svg')
class_name CursorDual
extends Sprite2D
# TODO: instead of setting the target tilemap manually,
# just add the CursorDual as a child of the target tilemap
@export var tilemap_dual: TileMapDual = null
var cell: Vector2i
var tile_size: Vector2
var sprite_size: Vector2
var terrain := 1
func _ready() -> void:
if tilemap_dual != null:
tile_size = tilemap_dual.tile_set.tile_size
sprite_size = self.texture.get_size()
scale = Vector2(tile_size.y, tile_size.y) / sprite_size
self.set_scale(scale)
func _process(_delta: float) -> void:
if tilemap_dual == null:
return
cell = tilemap_dual.local_to_map(tilemap_dual.get_local_mouse_position())
global_position = tilemap_dual.to_global(tilemap_dual.map_to_local(cell))
# Clicking the 1 key activates the first terrain
if Input.is_action_pressed("quick_action_1"):
terrain = 1
# Clicking the 2 key activates the second terrain
if Input.is_action_pressed("quick_action_2"):
terrain = 2
# Clicking the 0 key activates the background terrain
if Input.is_action_pressed("quick_action_0"):
terrain = 0
if Input.is_action_pressed("left_click"):
tilemap_dual.draw_cell(cell, terrain)
elif Input.is_action_pressed("right_click"):
tilemap_dual.draw_cell(cell, 0)

View File

@@ -0,0 +1 @@
uid://c6m630v880okx

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="CursorDual.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="35.5"
inkscape:cx="9.0845069"
inkscape:cy="8.112676"
inkscape:window-width="1366"
inkscape:window-height="736"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 8.9882813,2.0703125 V 4.046875 h 2.9648437 v 2.9648437 h 1.976563 V 2.0703125 Z"
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
id="path10" />
<path
d="m 8.9882813,11.953125 v 1.976563 H 13.929688 V 8.9882813 h -1.976563 v 2.9648437 z"
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
id="path9" />
<path
d="M 7.0117187,13.929688 V 11.953125 H 4.046875 V 8.9882813 H 2.0703125 v 4.9414067 z"
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
id="path8" />
<path
d="M 2.0703125,7.0117187 H 4.046875 V 4.046875 H 7.0117187 V 2.0703125 H 2.0703125 Z"
style="fill:#ffb273;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round"
id="path7" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bth8306ui4dcx"
path="res://.godot/imported/CursorDual.svg-af24e4bad2d22f0cc5627c83d35ae307.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/TileMapDual/CursorDual.svg"
dest_files=["res://.godot/imported/CursorDual.svg-af24e4bad2d22f0cc5627c83d35ae307.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

View File

@@ -0,0 +1,151 @@
##[br] A Node designed to hold and manage up to 2 DisplayLayer children.
##[br] See DisplayLayer.gd for details.
class_name Display
extends Node2D
## See TerrainDual.gd
var terrain: TerrainDual
## See TileSetWatcher.gd
var _tileset_watcher: TileSetWatcher
## The parent TileMapDual to base the terrains off of.
@export var world: TileMapDual
## Creates a new Display that updates when the TileSet updates.
func _init(world: TileMapDual, tileset_watcher: TileSetWatcher) -> void:
#print('initializing Display...')
self.world = world
_tileset_watcher = tileset_watcher
terrain = TerrainDual.new(tileset_watcher)
terrain.changed.connect(_terrain_changed, 1)
world_tiles_changed.connect(_world_tiles_changed, 1)
# let parent materal through to the displaylayers
use_parent_material = true
## Activates when the TerrainDual changes.
func _terrain_changed() -> void:
cached_cells.update(world)
_delete_layers()
if _tileset_watcher.tile_set != null:
_create_layers()
## Emitted when the tiles in the map have been edited.
signal world_tiles_changed(changed: Array)
func _world_tiles_changed(changed: Array) -> void:
#print('SIGNAL EMITTED: world_tiles_changed(%s)' % {'changed': changed})
for child in get_children(true):
child.update_tiles(cached_cells, changed)
## Initializes and configures new DisplayLayers according to the grid shape.
func _create_layers() -> void:
#print('GRID SHAPE: %s' % _tileset_watcher.grid_shape)
var grid: Array = GRIDS[_tileset_watcher.grid_shape]
for i in grid.size():
var layer_config: Dictionary = grid[i]
#print('layer_config: %s' % layer_config)
var layer := DisplayLayer.new(world, _tileset_watcher, layer_config, terrain.layers[i])
add_child(layer)
layer.update_tiles_all(cached_cells)
## Deletes all of the DisplayLayers.
func _delete_layers() -> void:
for child in get_children(true):
child.queue_free()
## The TileCache computed from the last time update() was called.
var cached_cells := TileCache.new()
## Updates the display based on the cells changed in the world TileMapDual.
func update(updated: Array) -> void:
if _tileset_watcher.tile_set == null:
return
_update_properties()
if not updated.is_empty():
cached_cells.update(world, updated)
world_tiles_changed.emit(updated)
## Activates when the properties of the parent TileMapDual have been edited.
func _update_properties() -> void:
for child in get_children(true):
child.update_properties(world)
# TODO: phase out GridShape and simply transpose everything when the offset axis is vertical
##[br] Returns what kind of grid a TileSet is.
##[br] Will default to SQUARE if Godot decides to add a new TileShape.
static func tileset_gridshape(tile_set: TileSet) -> GridShape:
var hori: bool = tile_set.tile_offset_axis == TileSet.TILE_OFFSET_AXIS_HORIZONTAL
match tile_set.tile_shape:
TileSet.TileShape.TILE_SHAPE_SQUARE:
return GridShape.SQUARE
TileSet.TileShape.TILE_SHAPE_ISOMETRIC:
return GridShape.ISO
TileSet.TileShape.TILE_SHAPE_HALF_OFFSET_SQUARE:
return GridShape.HALF_OFF_HORI if hori else GridShape.HALF_OFF_VERT
TileSet.TileShape.TILE_SHAPE_HEXAGON:
return GridShape.HEX_HORI if hori else GridShape.HEX_VERT
_:
return GridShape.SQUARE
## Every meaningfully different TileSet.tile_shape * TileSet.tile_offset_axis combination.
enum GridShape {
SQUARE,
ISO,
HALF_OFF_HORI,
HALF_OFF_VERT,
HEX_HORI,
HEX_VERT,
}
##[br] How to deal with every available GridShape.
##[br] See DisplayLayer.gd for more information about these fields.
const GRIDS: Dictionary = {
GridShape.SQUARE: [
{ # []
'offset': Vector2(-0.5, -0.5),
}
],
GridShape.ISO: [
{ # <>
'offset': Vector2(0, -0.5),
}
],
GridShape.HALF_OFF_HORI: [
{ # v
'offset': Vector2(0.0, -0.5),
},
{ # ^
'offset': Vector2(-0.5, -0.5),
},
],
GridShape.HALF_OFF_VERT: [
{ # >
'offset': Vector2(-0.5, 0.0),
},
{ # <
'offset': Vector2(-0.5, -0.5),
},
],
GridShape.HEX_HORI: [
{ # v
'offset': Vector2(0.0, -3.0 / 8.0),
},
{ # ^
'offset': Vector2(-0.5, -3.0 / 8.0),
},
],
GridShape.HEX_VERT: [
{ # >
'offset': Vector2(-3.0 / 8.0, 0.0),
},
{ # <
'offset': Vector2(-3.0 / 8.0, -0.5),
},
],
}

View File

@@ -0,0 +1 @@
uid://cacp8xe0jaxln

View File

@@ -0,0 +1,102 @@
##[br] A single TileMapLayer whose purpose is to display tiles to maintain the Dual Grid illusion.
##[br] Its contents are automatically computed and updated based on:
##[br] - the contents of the parent TileMapDual
##[br] - the rules set in its assigned TerrainLayer
class_name DisplayLayer
extends TileMapLayer
##[br] How much to offset this DisplayLayer relative to the main TileMapDual grid.
##[br] This is independent of tile size.
var offset: Vector2
## See TileSetWatcher.gd
var _tileset_watcher: TileSetWatcher
## See TerrainDual.gd
var _terrain: TerrainLayer
func _init(
world: TileMapDual,
tileset_watcher: TileSetWatcher,
fields: Dictionary,
layer: TerrainLayer
) -> void:
#print('initializing Layer...')
update_properties(world)
offset = fields.offset
_tileset_watcher = tileset_watcher
_terrain = layer
tile_set = tileset_watcher.tile_set
tileset_watcher.tileset_resized.connect(reposition, 1)
reposition()
## Adjusts the position of this DisplayLayer based on the tile set's tile_size
func reposition() -> void:
position = offset * Vector2(_tileset_watcher.tile_size)
## Copies properties from parent TileMapDual to child display tilemap
func update_properties(parent: TileMapDual) -> void:
# Both tilemaps must be the same, so we copy all relevant properties
# Tilemap
# already covered by parent._tileset_watcher
# Rendering
self.y_sort_origin = parent.y_sort_origin
self.x_draw_order_reversed = parent.x_draw_order_reversed
self.rendering_quadrant_size = parent.rendering_quadrant_size
# Physics
self.collision_enabled = parent.collision_enabled
self.use_kinematic_bodies = parent.use_kinematic_bodies
self.collision_visibility_mode = parent.collision_visibility_mode
# Navigation
self.navigation_enabled = parent.navigation_enabled
self.navigation_visibility_mode = parent.navigation_visibility_mode
# Canvas item properties
self.show_behind_parent = parent.show_behind_parent
self.top_level = parent.top_level
self.light_mask = parent.light_mask
self.visibility_layer = parent.visibility_layer
self.y_sort_enabled = parent.y_sort_enabled
self.modulate = parent.modulate
self.self_modulate = parent.self_modulate
# NOTE: parent material takes priority over the current shaders, causing the world tiles to show up
self.use_parent_material = parent.use_parent_material
# Save any manually introduced Material change:
self.material = parent.display_material
## Updates all display tiles to reflect the current changes.
func update_tiles_all(cache: TileCache) -> void:
update_tiles(cache, cache.cells.keys())
## Update all display tiles affected by the world cells
func update_tiles(cache: TileCache, updated_world_cells: Array) -> void:
#push_warning('updating tiles')
var already_updated := Set.new()
for path: Array in _terrain.display_to_world_neighborhood:
path = path.map(Util.reverse_neighbor)
for world_cell: Vector2i in updated_world_cells:
var display_cell := follow_path(world_cell, path)
if already_updated.insert(display_cell):
update_tile(cache, display_cell)
## Updates a specific world cell.
func update_tile(cache: TileCache, cell: Vector2i) -> void:
var get_cell_at_path := func(path): return cache.get_terrain_at(follow_path(cell, path))
var terrain_neighbors := _terrain.display_to_world_neighborhood.map(get_cell_at_path)
var mapping: Dictionary = _terrain.apply_rule(terrain_neighbors, cell)
var sid: int = mapping.sid
var tile: Vector2i = mapping.tile
set_cell(cell, sid, tile)
## Finds the neighbor of a given cell by following a path of CellNeighbors
func follow_path(cell: Vector2i, path: Array) -> Vector2i:
for neighbor: TileSet.CellNeighbor in path:
cell = get_neighbor_cell(cell, neighbor)
return cell

View File

@@ -0,0 +1 @@
uid://bon5inags0mqy

76
addons/TileMapDual/Set.gd Normal file
View File

@@ -0,0 +1,76 @@
##[br] Real sets don't exist yet.
##[br] https://github.com/godotengine/godot/pull/94399
class_name Set
extends Resource
## The internal Dictionary that holds this Set's items as keys.
var data: Dictionary = {}
func _init(initial_data: Variant = []) -> void:
union_in_place(initial_data)
## Returns true if the item exists in this Set.
func has(item: Variant) -> bool:
return item in data
## A dummy value to put in a slot.
const DUMMY = null
## Returns true if the item was not previously in the Set.
func insert(item: Variant) -> bool:
var out := not has(item)
data[item] = DUMMY
return out
## Returns true if the item was previously in the Set.
func remove(item: Variant) -> bool:
return data.erase(item)
## Deletes all items in this Set.
func clear() -> void:
data = {}
## Merges an Array's items or Dict's keys into the Set.
func union_in_place(other: Variant):
for item in other:
insert(item)
## Returns a new Set with the items of both self and other.
func union(other: Set) -> Set:
var out = self.duplicate()
out.union_in_place(other.data)
return out
## Removes an Array's items or Dict's keys from the Set.
func diff_in_place(other: Variant):
for item in other:
remove(item)
## Returns a new Set with all items in self that are not present in other.
func diff(other: Set) -> Set:
var out = self.duplicate()
out.diff_in_place(other.data)
return out
## Inserts elements that are in other but not in self, and removes elements found in both.
func xor_in_place(other: Variant):
for item in other:
if has(item):
remove(item)
else:
insert(item)
## Returns a new Set where each item is either in self or other, but not both.
func xor(other: Set) -> Set:
var out = self.duplicate()
out.xor_in_place(other.data)
return out

View File

@@ -0,0 +1 @@
uid://ds8j47h4vi6gx

View File

@@ -0,0 +1,194 @@
##[br] Reads a TileSet and dictates which tiles in the display map
##[br] match up with its neighbors in the world map
class_name TerrainDual
extends Resource
# Functions are ordered top to bottom in the transformation pipeline
## Maps a TileSet to a Neighborhood.
static func tileset_neighborhood(tile_set: TileSet) -> Neighborhood:
return GRID_NEIGHBORHOODS[Display.tileset_gridshape(tile_set)]
## Maps a GridShape to a Neighborhood.
const GRID_NEIGHBORHOODS = {
Display.GridShape.SQUARE: Neighborhood.SQUARE,
Display.GridShape.ISO: Neighborhood.ISOMETRIC,
Display.GridShape.HALF_OFF_HORI: Neighborhood.TRIANGLE_HORIZONTAL,
Display.GridShape.HALF_OFF_VERT: Neighborhood.TRIANGLE_VERTICAL,
Display.GridShape.HEX_HORI: Neighborhood.TRIANGLE_HORIZONTAL,
Display.GridShape.HEX_VERT: Neighborhood.TRIANGLE_VERTICAL,
}
## A specific neighborhood that the Display tiles will look at.
enum Neighborhood {
SQUARE,
ISOMETRIC,
TRIANGLE_HORIZONTAL,
TRIANGLE_VERTICAL,
}
## Maps a Neighborhood to a set of atlas terrain neighbors.
const NEIGHBORHOOD_LAYERS := {
Neighborhood.SQUARE: [
{ # []
'terrain_neighborhood': [
TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER,
TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER,
],
'display_to_world_neighborhood': [
[TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER],
[TileSet.CELL_NEIGHBOR_TOP_SIDE],
[TileSet.CELL_NEIGHBOR_LEFT_SIDE],
[],
],
},
],
Neighborhood.ISOMETRIC: [
{ # <>
'terrain_neighborhood': [
TileSet.CELL_NEIGHBOR_TOP_CORNER,
TileSet.CELL_NEIGHBOR_RIGHT_CORNER,
TileSet.CELL_NEIGHBOR_LEFT_CORNER,
TileSet.CELL_NEIGHBOR_BOTTOM_CORNER,
],
'display_to_world_neighborhood': [
[TileSet.CELL_NEIGHBOR_TOP_CORNER],
[TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE],
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
[],
],
},
],
Neighborhood.TRIANGLE_HORIZONTAL: [
{ # v
'terrain_neighborhood': [
TileSet.CELL_NEIGHBOR_BOTTOM_CORNER,
TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER,
TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
],
'display_to_world_neighborhood': [
[],
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
[TileSet.CELL_NEIGHBOR_TOP_RIGHT_SIDE],
],
},
{ # ^
'terrain_neighborhood': [
TileSet.CELL_NEIGHBOR_TOP_CORNER,
TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER,
],
'display_to_world_neighborhood': [
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
[TileSet.CELL_NEIGHBOR_LEFT_SIDE],
[],
],
},
],
# TODO: this is just TRIANGLE_HORIZONTAL but transposed. this can be refactored.
Neighborhood.TRIANGLE_VERTICAL: [
{ # >
'terrain_neighborhood': [
TileSet.CELL_NEIGHBOR_RIGHT_CORNER,
TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER,
TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
],
'display_to_world_neighborhood': [
[],
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
[TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE],
],
},
{ # <
'terrain_neighborhood': [
TileSet.CELL_NEIGHBOR_LEFT_CORNER,
TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER,
],
'display_to_world_neighborhood': [
[TileSet.CELL_NEIGHBOR_TOP_LEFT_SIDE],
[TileSet.CELL_NEIGHBOR_TOP_SIDE],
[],
],
},
],
}
## The Neighborhood type of this TerrainDual.
var neighborhood: Neighborhood
## Maps a terrain type to its sprite as registered in the TerrainDual.
var terrains: Dictionary
## The TerrainLayers for this TerrainDual.
var layers: Array
var _tileset_watcher: TileSetWatcher
func _init(tileset_watcher: TileSetWatcher) -> void:
_tileset_watcher = tileset_watcher
_tileset_watcher.terrains_changed.connect(_changed, 1)
_changed()
##[br] Emitted when any of the terrains change.
##[br] NOTE: Prefer connecting to TerrainDual.changed instead of TileSetWatcher.terrains_changed.
func _changed() -> void:
#print('SIGNAL EMITTED: changed(%s)' % {})
read_tileset(_tileset_watcher.tile_set)
emit_changed()
## Create rules for every atlas in a TileSet.
func read_tileset(tile_set: TileSet) -> void:
terrains = {}
layers = []
neighborhood = Neighborhood.SQUARE # default
if tile_set == null:
return
neighborhood = tileset_neighborhood(tile_set)
layers = NEIGHBORHOOD_LAYERS[neighborhood].map(TerrainLayer.new)
for i in tile_set.get_source_count():
var sid := tile_set.get_source_id(i)
var src := tile_set.get_source(sid)
if src is not TileSetAtlasSource:
continue
read_atlas(src, sid)
## Create rules for every tile in an atlas.
func read_atlas(atlas: TileSetAtlasSource, sid: int) -> void:
var size = atlas.get_atlas_grid_size()
for y in size.y:
for x in size.x:
var tile := Vector2i(x, y)
# Take only existing tiles
if not atlas.has_tile(tile):
continue
read_tile(atlas, sid, tile)
## Add a new rule for a specific tile in an atlas.
func read_tile(atlas: TileSetAtlasSource, sid: int, tile: Vector2i) -> void:
var data := atlas.get_tile_data(tile, 0)
var mapping := {'sid': sid, 'tile': tile}
var terrain_set := data.terrain_set
if terrain_set != 0:
push_warning(
"The tile at %s has a terrain set of %d. Only terrain set 0 is supported." % [mapping, terrain_set]
)
return
var terrain := data.terrain
if terrain != -1:
if not terrain in terrains:
terrains[terrain] = mapping
var filters = NEIGHBORHOOD_LAYERS[neighborhood]
for i in layers.size():
var layer: TerrainLayer = layers[i]
layer._register_tile(data, mapping)

View File

@@ -0,0 +1 @@
uid://qa8xnqphuk68

View File

@@ -0,0 +1,128 @@
## A set of _rules usable by a single DisplayLayer.
class_name TerrainLayer
extends Resource
## A list of which CellNeighbors to care about during terrain checking.
var terrain_neighborhood: Array = []
##[br] When a cell in a DisplayLayer needs to be recomputed,
## the TerrainLayer needs to know which tiles surround it.
##[br] This Array stores the paths from the affected cell to the neighboring world cells.
var display_to_world_neighborhood: Array
# TODO: change Mapping to support https://github.com/pablogila/TileMapDual/issues/13
##[br] The rules that dictate which tile matches a given set of neighbors.
##[br] Used by register_rule() and apply_rule()
##[codeblock]
## _rules: TrieNode = # The actual type of _rules
##
## TrieNode: Dictionary{
## key: int = # The terrain value of this neighbor
## value: TrieNode | TrieLeaf = # The next branch of this trie
## }
##
## TrieLeaf: Dictionary{
## 'mapping': Mapping = # The tile that this should now become
## }
##
## Mapping: Dictionary{
## 'sid': int = # The source_id of this tile
## 'tile': Vector2i = # The Atlas Coordinates of this tile
## }
##[/codeblock]
##[br] Internally a decision "trie":
##[br] - each node branch represents a terrain neighbor
##[br] - each leaf node represents the terrain that a tile
## with the given terrain neighborhood should become
##[br]
##[br] How the trie is searched:
##[br] - check the next neighbor in terrain_neighbors
##[br] - check if there is a branch corresponding to the terrain of that neighbor
##[br] - if there is a branch, search again under that branch
##[br] - else pretend that neighbor is empty and try again
##[br] - if there really isn't a branch, no rules exist so just return empty
##[br] - once at a leaf node, its mapping should tell us what terrain to become
##[br]
##[br] See apply_rule() for more details.
var _rules: Dictionary = {}
## Generator for random placement of tiles
var rand : RandomNumberGenerator
## Random seed used to generate tiles deterministically
var global_seed : int = 707
func _init(fields: Dictionary) -> void:
self.terrain_neighborhood = fields.terrain_neighborhood
self.display_to_world_neighborhood = fields.display_to_world_neighborhood
self.rand = RandomNumberGenerator.new()
## Register a new rule for a specific tile in an atlas.
func _register_tile(data: TileData, mapping: Dictionary) -> void:
if data.terrain_set != 0:
# This was already handled as an error in the parent TerrainDual
return
var terrain_neighbors := terrain_neighborhood.map(data.get_terrain_peering_bit)
# Skip tiles with no peering bits in this filter
# They might be used for a different layer,
# or may have no peering bits at all, which will just be ignored by all layers
if terrain_neighbors.any(func(neighbor): return neighbor == -1):
if terrain_neighbors.any(func(neighbor): return neighbor != -1):
push_warning(
"Invalid Tile Neighborhood at %s.\n" % [mapping] +
"Expected neighborhood: %s" % [terrain_neighborhood.map(Util.neighbor_name)]
)
return
mapping["prob"] = data.probability
_register_rule(terrain_neighbors, mapping)
## Register a new rule for a set of surrounding terrain neighbors
func _register_rule(terrain_neighbors: Array, mapping: Dictionary) -> void:
var node := _rules
for terrain in terrain_neighbors:
if terrain not in node:
node[terrain] = {}
node = node[terrain]
if not 'mappings' in node:
node.mappings = []
node.mappings.append(mapping)
const TILE_EMPTY: Dictionary = {'sid': - 1, 'tile': Vector2i(-1, -1)}
## Returns the tile that should be used based on the surrounding terrain neighbors
func apply_rule(terrain_neighbors: Array, cell: Vector2i) -> Dictionary:
var is_empty := terrain_neighbors.all(func(terrain): return terrain == -1)
if is_empty:
return TILE_EMPTY
var normalized_neighbors = terrain_neighbors.map(normalize_terrain)
var node := _rules
for terrain in normalized_neighbors:
if terrain not in node:
terrain = 0
if terrain not in node:
return TILE_EMPTY
node = node[terrain]
if 'mappings' not in node:
return TILE_EMPTY
#random tile selection
var weights: Array= []
var index: int = 0
for mapping in node.mappings:
weights.append(mapping.prob)
rand.seed = hash(str(cell)+str(global_seed))
index = rand.rand_weighted(weights)
return node.mappings[index]
## Coerces all empty tiles to have a terrain of 0.
static func normalize_terrain(terrain):
return terrain if terrain != -1 else 0
## Utility function for easier printing
func _neighbors_to_dict(terrain_neighbors: Array) -> Dictionary:
return Util.arrays_to_dict(terrain_neighborhood.map(Util.neighbor_name), terrain_neighbors)

View File

@@ -0,0 +1 @@
uid://ks3fxwahkm6j

View File

@@ -0,0 +1,322 @@
## Functions for automatically generating terrains for an atlas.
class_name TerrainPreset
## Maps a Neighborhood to a Topology.
const NEIGHBORHOOD_TOPOLOGIES := {
TerrainDual.Neighborhood.SQUARE: Topology.SQUARE,
TerrainDual.Neighborhood.ISOMETRIC: Topology.SQUARE,
TerrainDual.Neighborhood.TRIANGLE_HORIZONTAL: Topology.TRIANGLE,
TerrainDual.Neighborhood.TRIANGLE_VERTICAL: Topology.TRIANGLE,
}
## Determines the available Terrain presets for a certain Atlas.
enum Topology {
SQUARE,
TRIANGLE,
}
## Maps a Neighborhood to a preset of the specified name.
static func neighborhood_preset(
neighborhood: TerrainDual.Neighborhood,
preset_name: String = 'Standard'
) -> Dictionary:
var topology: Topology = NEIGHBORHOOD_TOPOLOGIES[neighborhood]
# TODO: test when the preset doesn't exist
var available_presets = PRESETS[topology]
if preset_name not in available_presets:
return {'size': Vector2i.ONE, 'layers': []}
var out: Dictionary = available_presets[preset_name].duplicate(true)
# All Horizontal neighborhoods can be transposed to Vertical
if neighborhood == TerrainDual.Neighborhood.TRIANGLE_VERTICAL:
out.size = Util.transpose_vec(out.size)
out.fg = Util.transpose_vec(out.fg)
out.bg = Util.transpose_vec(out.bg)
for seq in out.layers:
for i in seq.size():
seq[i] = Util.transpose_vec(seq[i])
return out
## Contains all of the builtin Terrain presets for each topology
const PRESETS := {
Topology.SQUARE: {
'Standard': {
'size': Vector2i(4, 4),
'bg': Vector2i(0, 3),
'fg': Vector2i(2, 1),
'layers': [
[ # []
Vector2i(0, 3),
Vector2i(3, 3),
Vector2i(0, 2),
Vector2i(1, 2),
Vector2i(0, 0),
Vector2i(3, 2),
Vector2i(2, 3),
Vector2i(3, 1),
Vector2i(1, 3),
Vector2i(0, 1),
Vector2i(1, 0),
Vector2i(2, 2),
Vector2i(3, 0),
Vector2i(2, 0),
Vector2i(1, 1),
Vector2i(2, 1),
],
],
},
},
Topology.TRIANGLE: {
'Standard': {
'size': Vector2i(4, 4),
'bg': Vector2i(0, 0),
'fg': Vector2i(0, 2),
'layers': [
[ # v
Vector2i(0, 1),
Vector2i(2, 3),
Vector2i(3, 1),
Vector2i(1, 3),
Vector2i(1, 1),
Vector2i(3, 3),
Vector2i(2, 1),
Vector2i(0, 3),
],
[ # ^
Vector2i(0, 0),
Vector2i(2, 2),
Vector2i(3, 0),
Vector2i(1, 2),
Vector2i(1, 0),
Vector2i(3, 2),
Vector2i(2, 0),
Vector2i(0, 2),
],
]
},
# Old template.
# a bit inconvenient to use for Brick (Half-Off Square) tilesets.
'Winged': {
'size': Vector2i(4, 4),
'bg': Vector2i(0, 0),
'fg': Vector2i(0, 2),
'layers': [
[ # v
Vector2i(0, 1),
Vector2i(2, 1),
Vector2i(3, 1),
Vector2i(1, 3),
Vector2i(1, 1),
Vector2i(3, 3),
Vector2i(2, 3),
Vector2i(0, 3),
],
[ # ^
Vector2i(0, 0),
Vector2i(2, 0),
Vector2i(3, 0),
Vector2i(1, 2),
Vector2i(1, 0),
Vector2i(3, 2),
Vector2i(2, 2),
Vector2i(0, 2),
],
]
},
# Old template.
# The gaps between triangles made them harder to align.
'Alternating': {
'size': Vector2i(4, 4),
'bg': Vector2i(0, 0),
'fg': Vector2i(0, 2),
'layers': [
[ # v
Vector2i(0, 0),
Vector2i(2, 0),
Vector2i(3, 1),
Vector2i(1, 3),
Vector2i(1, 1),
Vector2i(3, 3),
Vector2i(2, 2),
Vector2i(0, 2),
],
[ # ^
Vector2i(0, 1),
Vector2i(2, 1),
Vector2i(3, 0),
Vector2i(1, 2),
Vector2i(1, 0),
Vector2i(3, 2),
Vector2i(2, 3),
Vector2i(0, 3),
],
],
},
},
}
##[br] Would you like to automatically create tiles in the atlas?
##[br]
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
static func write_default_preset(urm: EditorUndoRedoManager, tile_set: TileSet, atlas: TileSetAtlasSource) -> void:
#print('writing default')
var neighborhood := TerrainDual.tileset_neighborhood(tile_set)
var terrain := new_terrain(
urm,
tile_set,
atlas.texture.resource_path.get_file()
)
write_preset(
urm,
atlas,
neighborhood,
terrain,
)
##[br] Creates terrain set 0 (the primary terrain set) and terrain 0 (the 'any' terrain)
##[br]
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
static func init_terrains(urm: EditorUndoRedoManager, tile_set: TileSet) -> void:
urm.add_do_method(TerrainPreset, "_do_init_terrains", tile_set)
urm.add_undo_method(TerrainPreset, "_undo_init_terrains", tile_set)
static func _do_init_terrains(tile_set: TileSet) -> void:
tile_set.add_terrain_set()
tile_set.set_terrain_set_mode(0, TileSet.TERRAIN_MODE_MATCH_CORNERS)
tile_set.add_terrain(0)
tile_set.set_terrain_name(0, 0, "<any>")
tile_set.set_terrain_color(0, 0, Color.VIOLET)
static func _undo_init_terrains(tile_set: TileSet) -> void:
tile_set.remove_terrain_set(0)
##[br] Adds a new terrain type to terrain set 0 for the sprites to use.
##[br]
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
static func new_terrain(urm: EditorUndoRedoManager, tile_set: TileSet, terrain_name: String) -> int:
var terrain: int
if tile_set.get_terrain_sets_count() == 0:
init_terrains(urm, tile_set)
terrain = 1
else:
terrain = tile_set.get_terrains_count(0)
urm.add_do_method(TerrainPreset, "_do_new_terrain", tile_set, terrain_name)
urm.add_undo_method(TerrainPreset, "_undo_new_terrain", tile_set)
return terrain
static func _do_new_terrain(tile_set: TileSet, terrain_name: String) -> void:
tile_set.add_terrain(0)
var terrain := tile_set.get_terrains_count(0) - 1
tile_set.set_terrain_name(0, terrain, "FG -%s" % terrain_name)
static func _undo_new_terrain(tile_set: TileSet) -> void:
var terrain := tile_set.get_terrains_count(0) - 1
tile_set.remove_terrain(0, terrain)
##[br] Takes a preset and writes it onto the given atlas, replacing the previous configuration.
##[br] ARGUMENTS:
##[br] - atlas: the atlas source to apply the preset to.
##[br] - filters: the neighborhood filter
##[br]
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
static func write_preset(
urm: EditorUndoRedoManager,
atlas: TileSetAtlasSource,
neighborhood: TerrainDual.Neighborhood,
terrain_foreground: int,
terrain_background: int = 0,
preset: Dictionary = neighborhood_preset(neighborhood),
) -> void:
clear_and_divide_atlas(urm, atlas, preset.size)
urm.add_do_method(TerrainPreset, '_do_write_preset', atlas, neighborhood, terrain_foreground, terrain_background, preset)
static func _do_write_preset(
atlas: TileSetAtlasSource,
neighborhood: TerrainDual.Neighborhood,
terrain_foreground: int,
terrain_background: int,
preset: Dictionary,
) -> void:
var layers: Array = TerrainDual.NEIGHBORHOOD_LAYERS[neighborhood]
# Set peering bits
var sequences: Array = preset.layers
for j in layers.size():
var terrain_neighborhood = layers[j].terrain_neighborhood
var sequence: Array = sequences[j]
for i in sequence.size():
var tile: Vector2i = sequence[i]
atlas.create_tile(tile)
var data := atlas.get_tile_data(tile, 0)
data.terrain_set = 0
for neighbor in terrain_neighborhood:
data.set_terrain_peering_bit(
neighbor,
[terrain_background, terrain_foreground][i & 1]
)
i >>= 1
# Set terrains
atlas.get_tile_data(preset.bg, 0).terrain = terrain_background
atlas.get_tile_data(preset.fg, 0).terrain = terrain_foreground
##[br] Unregisters all the tiles in an atlas and changes the size of the individual sprites.
##[br]
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
static func clear_and_resize_atlas(urm: EditorUndoRedoManager, atlas: TileSetAtlasSource, size: Vector2) -> void:
var atlas_data := _save_atlas_data(atlas)
urm.add_do_method(TerrainPreset, '_do_clear_and_resize_atlas', atlas, size)
urm.add_undo_method(TerrainPreset, '_undo_clear_and_resize_atlas', atlas, atlas_data)
static func _do_clear_and_resize_atlas(atlas: TileSetAtlasSource, size: Vector2) -> void:
# Clear all tiles
atlas.texture_region_size = atlas.texture.get_size() + Vector2.ONE
atlas.clear_tiles_outside_texture()
# Resize the tiles
atlas.texture_region_size = size
static func _undo_clear_and_resize_atlas(atlas: TileSetAtlasSource, atlas_data: Dictionary) -> void:
_load_atlas_data(atlas, atlas_data)
## NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else.
static func _save_atlas_data(atlas: TileSetAtlasSource) -> Dictionary:
var size_img := atlas.texture.get_size()
var size_sprite := atlas.texture_region_size
var size_dims := Vector2i(size_img) / size_sprite
var tiles := []
for y in size_dims.y:
var row := []
for x in size_dims.x:
var tile := Vector2i(x, y)
var exists := atlas.has_tile(tile)
row.push_back(exists)
tiles.push_back(row)
return {
'size_sprite': size_sprite,
'size_dims': size_dims,
'tiles': tiles,
}
static func _load_atlas_data(atlas: TileSetAtlasSource, atlas_data: Dictionary) -> void:
_do_clear_and_resize_atlas(atlas, atlas_data.size_sprite)
for y in atlas_data.size_dims.y:
for x in atlas_data.size_dims.x:
if atlas_data.tiles[y][x]:
var tile := Vector2i(x, y)
atlas.create_tile(tile)
##[br] Unregisters all the tiles in an atlas and changes the size of the
## individual sprites to accomodate a divisions.x by divisions.y grid of sprites.
##[br]
##[br] NOTE: Assumes urm.create_action() was called. Does not actually do anything until urm.commit_action() is called.
##[br] NOTE: Assumes atlas only has auto-generated tiles. Does not save peering bit information or anything else for undo/redo.
static func clear_and_divide_atlas(urm: EditorUndoRedoManager, atlas: TileSetAtlasSource, divisions: Vector2i) -> void:
clear_and_resize_atlas(urm, atlas, atlas.texture.get_size() / Vector2(divisions))

View File

@@ -0,0 +1 @@
uid://cdsxbtu57wews

View File

@@ -0,0 +1,74 @@
## Caches the sprite location and terrains of each tile in the TileMapDual world grid.
class_name TileCache
extends Resource
##[br] Maps a cell coordinate to the stored tile data.
##[codeblock]
## Dictionary{
## key: Vector2i = # The coordinates of this tile in the world grid.
## value: Dictionary{
## 'sid': int = # The Source ID of this tile.
## 'tile': Vector2i = # The coordinates of this tile in its Atlas.
## 'terrain': int = # The terrain assigned to this tile.
## } = # The data stored at this tile.
## }
##[/codeblock]
var cells := {}
func _init() -> void:
pass
##[br] Updates specific cells of the TileCache based on the current layer data at those points.
##[br] Makes corrections in case the user accidentally places invalid tiles.
func update(world: TileMapLayer, edited: Array = cells.keys()) -> void:
var tile_set := world.tile_set
if tile_set == null:
push_error('Attempted to update TileCache while tile set was null')
return
for cell in edited:
# Invalid cells will be treated as empty and ignored
var sid := world.get_cell_source_id(cell)
if sid == -1:
cells.erase(cell)
continue
if not tile_set.has_source(sid):
continue
var src = tile_set.get_source(sid)
var tile := world.get_cell_atlas_coords(cell)
if not src.has_tile(tile):
continue
var data := world.get_cell_tile_data(cell)
if data == null:
continue
# Accidental cells should be reset to their previous value
# They will be treated as unchanged
if data.terrain == -1 or data.terrain_set != 0:
if cell not in cells:
world.erase_cell(cell)
continue
var cached: Dictionary = cells[cell]
sid = cached.sid
tile = cached.tile
world.set_cell(cell, sid, tile)
data = world.get_cell_tile_data(cell)
cells[cell] = {'sid': sid, 'tile': tile, 'terrain': data.terrain}
## Returns the symmetric difference (xor) of two tile caches.
func xor(other: TileCache) -> Array[Vector2i]:
var out: Array[Vector2i] = []
for key in cells:
if key not in other.cells or cells[key].terrain != other.cells[key].terrain:
out.push_back(key)
for key in other.cells:
if key not in cells:
out.push_back(key)
return out
##[br] Returns the terrain value of the tile at the given cell coordinates.
##[br] Empty cells have a terrain of -1.
func get_terrain_at(cell: Vector2i) -> int:
if cell not in cells:
return -1
return cells[cell].terrain

View File

@@ -0,0 +1 @@
uid://dpuc70ypq7wf8

View File

@@ -0,0 +1,160 @@
@tool
@icon('TileMapDual.svg')
class_name TileMapDual
extends TileMapLayer
## An invisible material used to hide the world grid so only the display layers show up.
## Currently implemented as a shader that sets all pixels to 0 alpha.
var _ghost_material: Material = preload("res://addons/TileMapDual/ghost_material.tres")
# === External functions that don't exist once exported ===
# HACK: this uses some sort of "Dynamic Linking" technique because these features don't exist right now
# - conditional compilation
# - static signals
static func _editor_only(name: String):
push_error('Attempt to call Editor-Only function "' + name + '"')
static var autotile: Callable = _editor_only.bind('autotile').unbind(3)
static var popup: Callable = _editor_only.bind('popup').unbind(2)
## Material for the display tilemap.
@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "ShaderMaterial, CanvasItemMaterial")
var display_material: Material:
get:
return display_material
set(new_material): # Custom setter so that it gets copied
display_material = new_material
changed.emit()
var _tileset_watcher: TileSetWatcher
var _display: Display
func _ready() -> void:
_tileset_watcher = TileSetWatcher.new(tile_set)
_display = Display.new(self, _tileset_watcher)
add_child(_display)
_make_self_invisible(true)
if Engine.is_editor_hint():
_tileset_watcher.atlas_autotiled.connect(_atlas_autotiled)
set_process(true)
else: # Run in-game using signals for better performance
changed.connect(_changed, 1)
set_process(false)
# Update full tileset on first instance
await get_tree().process_frame
_changed()
## Automatically generate terrains when the atlas is initialized.
func _atlas_autotiled(source_id: int, atlas: TileSetAtlasSource):
autotile.call(source_id, atlas, tile_set)
## Keeps track of use_parent_material to see when it turns on or off.
var _cached_use_parent_material = null
##[br] Makes the main world grid invisible.
##[br] The main tiles don't need to be seen. Only the DisplayLayers should be visible.
##[br] Called every frame, and functions a lot like TileSetWatcher.
func _make_self_invisible(startup: bool = false) -> void:
# If user has set a material in the original slot, inform the user
if material != _ghost_material:
if not startup and Engine.is_editor_hint():
popup.call(
"Warning! Direct material edit detected.",
"Don't manually edit the real material in the editor! Instead edit the custom 'Display Material' property.\n" +
"(Resetting the material to an invisible shader material... this is to keep the 'World Layer' invisible)\n" +
"* This warning is only given when the material is set in the Godot editor.\n" +
"* In-game scripts may set the material directly. It will be copied over to display_material automatically."
)
else:
# copy over the material if it was edited by script
display_material = material
material = _ghost_material # Force TileMapDual's material to become invisible
# check if use_parent_material is set
if (
Engine.is_editor_hint()
and use_parent_material != _cached_use_parent_material
and _cached_use_parent_material == false # cache may be null
):
popup.call(
"Warning: Using Parent Material.",
"The parent material will override any other materials used by the TileMapDual,\n" +
"including the 'ghost shader' that the world tiles use to hide themselves.\n" +
"This will cause the world tiles to show themselves in-game.\n" +
"\n" +
"* Recommendation: Turn this setting off. Don't use parent material.\n" +
"* Workaround: Set your world tiles to custom sprites that are entirely transparent.\n" +
"(see 'res://addons/TileMapDual/docs/custom_drawing_sprites.mp4' for a non-transparent example)"
)
_cached_use_parent_material = use_parent_material
## HACK: How long to wait before processing another "frame".
## Mainly matters when [godot_4_3_compatibility] is active.
@export_range(0.0, 0.1) var refresh_time: float = 0.02
var _timer: float = 0.0
func _process(delta: float) -> void: # Only used inside the editor
if refresh_time < 0.0:
return
if _timer > 0:
_timer -= delta
return
_timer = refresh_time
call_deferred('_changed')
## When toggled on, double-checks ALL cells in the grid every change.
## Only use this when running Godot 4.3 and below,
## where TileMapLayer could not detect changes properly.
@export var godot_4_3_compatibility: bool = _godot_is_below_4_4()
## Detects if godot is below v4.4.
## Only used to detect whether the _update_cells() function is usable.
func _godot_is_below_4_4():
var version := Engine.get_version_info()
return version.major < 4 or version.major == 4 and version.minor < 4
## Called by signals when the tileset changes,
## or by _process inside the editor.
func _changed() -> void:
_tileset_watcher.update(tile_set)
var updated_cells := []
# HACK: double check all tiles every refresh
if godot_4_3_compatibility and tile_set != null:
var current_cells := TileCache.new()
current_cells.update(self, get_used_cells())
updated_cells = current_cells.xor(_display.cached_cells)
_display.update(updated_cells)
_make_self_invisible()
## Called when the user draws on the map or presses undo/redo.
func _update_cells(coords: Array[Vector2i], forced_cleanup: bool) -> void:
if is_instance_valid(_display):
_display.update(coords)
##[br] Public method to add and remove tiles.
##[br]
##[br] - 'cell' is a vector with the cell position.
##[br] - 'terrain' is which terrain type to draw.
##[br] - terrain -1 completely removes the tile,
##[br] - and by default, terrain 0 is the empty tile.
func draw_cell(cell: Vector2i, terrain: int = 1) -> void:
var terrains := _display.terrain.terrains
if terrain not in terrains:
erase_cell(cell)
changed.emit()
return
var tile_to_use: Dictionary = terrains[terrain]
var sid: int = tile_to_use.sid
var tile: Vector2i = tile_to_use.tile
set_cell(cell, sid, tile)
changed.emit()
## Public method to get the terrain at a specific coordinate.
func get_cell(cell: Vector2i) -> int:
return get_cell_tile_data(cell).terrain

View File

@@ -0,0 +1 @@
uid://cjk8nronimk5r

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="TileMapDual.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="35.5"
inkscape:cx="8"
inkscape:cy="8.028169"
inkscape:window-width="1366"
inkscape:window-height="736"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill="#8da5f3"
d="M8 2 6.25 3.375 8 4.75l1.75-1.375zm2.624 2.062-1.75 1.375 1.75 1.375 1.75-1.375Zm2.626 2.063L11.5 7.5l1.75 1.375L15 7.5ZM5.376 4.062l-1.75 1.375 1.75 1.375 1.75-1.375zM8 6.124 6.25 7.499 8 8.874l1.75-1.375zm2.626 2.063-1.75 1.375 1.75 1.375 1.75-1.375ZM2.75 6.125 1 7.5l1.75 1.375L4.5 7.5Zm2.624 2.062-1.75 1.375 1.75 1.375 1.75-1.375ZM8 10.25l-1.75 1.375L8 13l1.75-1.375z"
id="path1"
style="fill:#ffb273;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bqu55yxko3ofm"
path="res://.godot/imported/TileMapDual.svg-74ed91a3d196dbe51dce1133c9b2d6f7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/TileMapDual/TileMapDual.svg"
dest_files=["res://.godot/imported/TileMapDual.svg-74ed91a3d196dbe51dce1133c9b2d6f7.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

View File

@@ -0,0 +1,333 @@
@tool
@icon('TileMapDual.svg')
class_name TileMapDualLegacy
extends TileMapLayer
## Material for the display tilemap.
@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "ShaderMaterial,CanvasItemMaterial")
var display_material: Material:
get:
return display_material
set(new_material): # Custom setter so that it gets copied
display_material = new_material
if display_tilemap: # Copy over, only if display tilemap is initiated
display_tilemap.material = display_material
var display_tilemap: TileMapLayer = null
var _filled_cells: Dictionary = {}
var _emptied_cells: Dictionary = {}
var _tile_shape: TileSet.TileShape = TileSet.TileShape.TILE_SHAPE_SQUARE
var _tile_size: Vector2i = Vector2i(16, 16)
## Coordinates for the fully-filled tile in the Atlas that
## will be used to sketch in the World grid.
## Only this tile will be considered for autotiling.
var full_tile: Vector2i = Vector2i(2,1)
## The opposed of full_tile. Used to erase sketched tiles.
var empty_tile: Vector2i = Vector2i(0,3)
var _should_check_cells: bool = false
## Prevents checking the cells more than once when the entire tileset
## is being updated, which is indicated by `_should_check_cells`.
var _checked_cells: Dictionary = {}
var is_isometric: bool = false
var _atlas_id: int
## We will use a bit-wise logic, so that a summation over all sketched
## neighbours provides a unique key, assigned to the corresponding
## tile from the Atlas through the NEIGHBOURS_TO_ATLAS dictionary.
enum _location {
TOP_LEFT = 1,
LOW_LEFT = 2,
TOP_RIGHT = 4,
LOW_RIGHT = 8
}
enum _direction {
TOP,
LEFT,
BOTTOM,
RIGHT,
BOTTOM_LEFT,
BOTTOM_RIGHT,
TOP_LEFT,
TOP_RIGHT,
}
## Overlapping tiles from the World grid
## that a tile from the Dual grid has.
const _NEIGHBORS := {
_direction.TOP : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_SIDE,
_direction.LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_LEFT_SIDE,
_direction.RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_RIGHT_SIDE,
_direction.BOTTOM : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_SIDE,
_direction.TOP_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_LEFT_CORNER,
_direction.TOP_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_RIGHT_CORNER,
_direction.BOTTOM_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER,
_direction.BOTTOM_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER
}
## Overlapping tiles from the World grid
## that a tile from the Dual grid has.
## To be used ONLY with isometric tilesets.
## CellNighbors are literal, even for Isometric
const _NEIGHBORS_ISOMETRIC := {
_direction.TOP : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_RIGHT_SIDE,
_direction.LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_LEFT_SIDE,
_direction.RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_RIGHT_SIDE,
_direction.BOTTOM : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_LEFT_SIDE,
_direction.TOP_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_TOP_CORNER,
_direction.TOP_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_RIGHT_CORNER,
_direction.BOTTOM_LEFT : TileSet.CellNeighbor.CELL_NEIGHBOR_LEFT_CORNER,
_direction.BOTTOM_RIGHT : TileSet.CellNeighbor.CELL_NEIGHBOR_BOTTOM_CORNER
}
## Dict to assign the Atlas coordinates from the
## summation over all sketched NEIGHBOURS.
## Follows the official 2x2 template.
## Works for isometric as well.
const _NEIGHBORS_TO_ATLAS: Dictionary = {
0: Vector2i(0,3),
1: Vector2i(3,3),
2: Vector2i(0,0),
3: Vector2i(3,2),
4: Vector2i(0,2),
5: Vector2i(1,2),
6: Vector2i(2,3),
7: Vector2i(3,1),
8: Vector2i(1,3),
9: Vector2i(0,1),
10: Vector2i(3,0),
11: Vector2i(2,0),
12: Vector2i(1,0),
13: Vector2i(2,2),
14: Vector2i(1,1),
15: Vector2i(2,1)
}
func _ready() -> void:
if Engine.is_editor_hint():
set_process(true)
else: # Run in-game using signals for better performance
set_process(false)
self.changed.connect(_update_tileset, 1)
update_full_tileset()
# Fire copy upon any property change. More specific than _update_tileset
self.changed.connect(copy_properties, 1)
func _process(_delta): # Only used inside the editor
if not self.tile_set:
return
call_deferred('_update_tileset')
## Copies properties from parent TileMapDual to child display tilemap
func copy_properties() -> void:
# Both tilemaps must be the same, so we copy all relevant properties
# Tilemap
display_tilemap.tile_set = self.tile_set
# Rendering
display_tilemap.y_sort_origin = self.y_sort_origin
display_tilemap.x_draw_order_reversed = self.x_draw_order_reversed
display_tilemap.rendering_quadrant_size = self.rendering_quadrant_size
# Physics
display_tilemap.collision_enabled = self.collision_enabled
display_tilemap.use_kinematic_bodies = self.use_kinematic_bodies
display_tilemap.collision_visibility_mode = self.collision_visibility_mode
# Navigation
display_tilemap.navigation_enabled = self.navigation_enabled
display_tilemap.navigation_visibility_mode = self.navigation_visibility_mode
# Canvas item properties
display_tilemap.show_behind_parent = self.show_behind_parent
display_tilemap.top_level = self.top_level
display_tilemap.light_mask = self.light_mask
display_tilemap.visibility_layer = self.visibility_layer
display_tilemap.y_sort_enabled = self.y_sort_enabled
display_tilemap.modulate = self.modulate
# If user has set a material in the original slot, copy it over for redundancy
# Helps both migration to new version, and prevents user mistakes
if self.material:
display_material = self.material
# Set material for first time
display_tilemap.material = display_material
# Save any manually introduced alpha modulation:
if self.self_modulate.a != 0.0:
display_tilemap.self_modulate = self.self_modulate
self.self_modulate.a = 0.0 # Override modulation to prevent render bugs with certain shaders
self.material = null # Unset TileMapDual's material, to prevent render of it
## Set the dual grid as a child of TileMapDual.
func _set_display_tilemap() -> void:
if not self.tile_set:
return
# Add the display TileMapLayer, if it doesn't already exist
if not get_node_or_null("WorldTileMap"):
display_tilemap = TileMapLayer.new()
display_tilemap.name = "WorldTileMap"
add_child(display_tilemap)
copy_properties() # Copy properties from TileMapDual to displayed tilemap
update_geometry()
display_tilemap.clear()
## Update the size and shape of the tileset, displacing the display TileMapLayer accordingly.
func update_geometry() -> void:
is_isometric = self.tile_set.tile_shape == TileSet.TileShape.TILE_SHAPE_ISOMETRIC
var offset := Vector2(self.tile_set.tile_size) * -0.5
if is_isometric:
offset.x = 0
display_tilemap.position = offset
_tile_size = self.tile_set.tile_size
_tile_shape = self.tile_set.tile_shape
## Update the entire tileset, processing all the cells in the map.
func update_full_tileset() -> void:
if display_tilemap == null:
_set_display_tilemap()
elif display_tilemap.tile_set != self.tile_set: # TO-DO: merge with the above
_set_display_tilemap()
_should_check_cells = true
for _cell in self.get_used_cells():
if _is_world_tile_sketched(_cell) == 1 or _is_world_tile_sketched(_cell) == 0:
update_tile(_cell)
_should_check_cells = false
_checked_cells = {}
# _checked_cells is only used when updating
# the whole tilemap to avoid repeating checks.
# This is skipped when updating tiles individually.
## Update only the very specific tiles that have changed.
## Much more efficient than update_full_tileset.
## Called by signals when the tileset changes,
## or by _process inside the editor.
func _update_tileset() -> void:
if display_tilemap == null:
update_full_tileset()
return
elif display_tilemap.tile_set != self.tile_set: # TO-DO: merge with the above
update_full_tileset()
return
elif _tile_size != self.tile_set.tile_size or _tile_shape != self.tile_set.tile_shape:
update_geometry()
return
var _new_emptied_cells: Dictionary = array_to_dict(get_used_cells_by_id(-1, empty_tile))
var _new_filled_cells: Dictionary = array_to_dict(get_used_cells_by_id(-1, full_tile))
var _changed_cells: Dictionary = exclude_dicts(_emptied_cells, _new_emptied_cells).merged(exclude_dicts(_filled_cells, _new_filled_cells))
_emptied_cells = _new_emptied_cells
_filled_cells = _new_filled_cells
for _cell in _changed_cells:
update_tile(_cell)
func array_to_dict(array: Array) -> Dictionary:
var dict: Dictionary = {}
for item in array:
dict[item] = true
return dict
## Return the values that are not shared between the arrays
func exclude_dicts(a: Dictionary, b: Dictionary) -> Dictionary:
var result = a.duplicate()
for item in b:
if result.has(item):
result.erase(item)
else:
result[item] = true
return result
## Takes a cell, and updates the overlapping tiles from the dual grid accordingly.
func update_tile(world_cell: Vector2i, recurse: bool = true) -> void:
_atlas_id = self.get_cell_source_id(world_cell)
# to not fall in a recursive loop because of a large space of emptiness in the map
if (!recurse and _atlas_id == -1):
return
var __NEIGHBORS = _NEIGHBORS_ISOMETRIC if is_isometric else _NEIGHBORS
var _top_left = world_cell
var _low_left = display_tilemap.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM])
var _top_right = display_tilemap.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.RIGHT])
var _low_right = display_tilemap.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM_RIGHT])
_update_displayed_tile(_top_left)
_update_displayed_tile(_low_left)
_update_displayed_tile(_top_right)
_update_displayed_tile(_low_right)
# if atlas id is -1 the tile is empty, so to have a good rendering we need to update surroundings
if (_atlas_id == -1):
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.LEFT]), false)
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.TOP_LEFT]), false)
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.TOP]), false)
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.TOP_RIGHT]), false)
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.RIGHT]), false)
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM_RIGHT]), false)
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM]), false)
update_tile(self.get_neighbor_cell(world_cell, __NEIGHBORS[_direction.BOTTOM_LEFT]), false)
func _update_displayed_tile(_display_cell: Vector2i) -> void:
# Avoid updating cells more than necessary
if _should_check_cells:
if _display_cell in _checked_cells:
return
_checked_cells[_display_cell] = true
var __NEIGHBORS = _NEIGHBORS_ISOMETRIC if is_isometric else _NEIGHBORS
var _top_left = display_tilemap.get_neighbor_cell(_display_cell, __NEIGHBORS[_direction.TOP_LEFT])
var _low_left = display_tilemap.get_neighbor_cell(_display_cell, __NEIGHBORS[_direction.LEFT])
var _top_right = display_tilemap.get_neighbor_cell(_display_cell, __NEIGHBORS[_direction.TOP])
var _low_right = _display_cell
# We perform a bitwise summation over the sketched neighbours
var _tile_key: int = 0
if _is_world_tile_sketched(_top_left) == 1:
_tile_key += _location.TOP_LEFT
if _is_world_tile_sketched(_low_left) == 1:
_tile_key += _location.LOW_LEFT
if _is_world_tile_sketched(_top_right) == 1:
_tile_key += _location.TOP_RIGHT
if _is_world_tile_sketched(_low_right) == 1:
_tile_key += _location.LOW_RIGHT
var _coords_atlas: Vector2i = _NEIGHBORS_TO_ATLAS[_tile_key]
display_tilemap.set_cell(_display_cell, _atlas_id, _coords_atlas)
## Return -1 if the cell is empty, 0 if sketched with the empty tile,
## and 1 if it is sketched with the fully-filled tile.
func _is_world_tile_sketched(_world_cell: Vector2i):
var _atlas_coords = get_cell_atlas_coords(_world_cell)
if _atlas_coords == full_tile:
return 1
elif _atlas_coords == empty_tile:
return 0
return -1
## Public method to add and remove tiles, as
## TileMapDual.draw(cell, tile, atlas_id).
## 'cell' is a vector with the cell position.
## 'tile' is 1 to draw the full tile (default), 0 to draw the empty tile,
## and -1 to completely remove the tile.
## 'atlas_id' is the atlas id of the tileset to modify, 0 by default.
## This method replaces the deprecated 'fill_tile' and 'erase_tile' methods.
func draw(cell: Vector2i, tile: int = 1, atlas_id: int = 0) -> void:
# Prevents a crash if this is called on the first frame
if display_tilemap == null:
update_full_tileset()
var tile_to_use: Vector2i
if tile == 1:
tile_to_use = full_tile
if tile == 0:
tile_to_use = empty_tile
if tile == -1:
tile_to_use = Vector2i(-1, -1)
atlas_id = -1
set_cell(cell, atlas_id, tile_to_use)
update_tile(cell)

View File

@@ -0,0 +1 @@
uid://dlyqj0u8ckhb0

View File

@@ -0,0 +1,181 @@
## Provides information about a TileSet and sends signals when it changes.
class_name TileSetWatcher
extends Resource
## Caches the previous tile_set to see when it changes.
var tile_set: TileSet
## Caches the previous tile_size to see when it changes.
var tile_size: Vector2i
## caches the previous result of display.tileset_grid_shape(tile_set) to see when it changes.
var grid_shape: Display.GridShape
func _init(tile_set: TileSet) -> void:
# tileset_deleted.connect(func(): print('tileset_deleted'), 1)
# tileset_created.connect(func(): print('tileset_created'), 1)
# tileset_resized.connect(func(): print('tileset_resized'), 1)
# tileset_reshaped.connect(func(): print('tileset_reshaped'), 1)
atlas_added.connect(_atlas_added, 1)
update(tile_set)
var _flag_tileset_deleted := false
## Emitted when the parent TileMapDual's tile_set is cleared or replaced.
signal tileset_deleted
var _flag_tileset_created := false
## Emitted when the parent TileMapDual's tile_set is created or replaced.
signal tileset_created
var _flag_tileset_resized := false
## Emitted when tile_set.tile_size is changed.
signal tileset_resized
var _flag_tileset_reshaped := false
## Emitted when the GridShape of the TileSet would be different.
signal tileset_reshaped
var _flag_atlas_added := false
## Emitted when a new Atlas is added to this TileSet.
## Does not react to Scenes being added to the TileSet.
signal atlas_added(source_id: int, atlas: TileSetAtlasSource)
func _atlas_added(source_id: int, atlas: TileSetAtlasSource) -> void:
_flag_atlas_added = true
#print('SIGNAL EMITTED: atlas_added(%s)' % {'source_id': source_id, 'atlas': atlas})
## Emitted when the watcher thinks that "Yes" was clicked for:
## 'Would you like to automatically create tiles in the atlas?'
signal atlas_autotiled(source_id: int, atlas: TileSetAtlasSource)
var _flag_terrains_changed := false
## Emitted when an atlas is added or removed,
## or when the terrains change in one of the Atlases.
## NOTE: Prefer connecting to TerrainDual.changed instead of TileSetWatcher.terrains_changed.
signal terrains_changed
## Checks if anything about the concerned TileMapDual's tile_set changed.
## Must be called by the TileMapDual every frame.
func update(tile_set: TileSet) -> void:
check_tile_set(tile_set)
check_flags()
## Emit update signals if the corresponding flags were set.
## Must only be run once per frame.
func check_flags() -> void:
if _flag_tileset_changed:
_flag_tileset_changed = false
_update_tileset()
if _flag_tileset_deleted:
_flag_tileset_deleted = false
_flag_tileset_reshaped = true
tileset_deleted.emit()
if _flag_tileset_created:
_flag_tileset_created = false
_flag_tileset_reshaped = true
tileset_created.emit()
if _flag_tileset_resized:
_flag_tileset_resized = false
tileset_resized.emit()
if _flag_tileset_reshaped:
_flag_tileset_reshaped = false
_flag_terrains_changed = true
tileset_reshaped.emit()
if _flag_atlas_added:
_flag_atlas_added = false
_flag_terrains_changed = true
if _flag_terrains_changed:
_flag_terrains_changed = false
terrains_changed.emit()
## Check if tile_set has been added, replaced, or deleted.
func check_tile_set(tile_set: TileSet) -> void:
if tile_set == self.tile_set:
return
if self.tile_set != null:
self.tile_set.changed.disconnect(_set_tileset_changed)
_cached_source_count = 0
_cached_sids.clear()
_flag_tileset_deleted = true
self.tile_set = tile_set
if self.tile_set != null:
self.tile_set.changed.connect(_set_tileset_changed, 1)
self.tile_set.emit_changed()
_flag_tileset_created = true
emit_changed()
var _flag_tileset_changed := false
## Helper method to be called when the tile_set detects a change.
## Must be disconnected when the tile_set is changed.
func _set_tileset_changed() -> void:
_flag_tileset_changed = true
## Called when _flag_tileset_changed.
## Provides more detail about what changed.
func _update_tileset() -> void:
var tile_size = tile_set.tile_size
if self.tile_size != tile_size:
self.tile_size = tile_size
_flag_tileset_resized = true
var grid_shape = Display.tileset_gridshape(tile_set)
if self.grid_shape != grid_shape:
self.grid_shape = grid_shape
_flag_tileset_reshaped = true
_update_tileset_atlases()
# Cached variables from the previous frame
# These are used to compare what changed between frames
var _cached_source_count: int = 0
var _cached_sids := {}
# TODO: detect automatic tile creation
## Checks if new atlases have been added.
## Does not check which ones were deleted.
func _update_tileset_atlases() -> void:
# Update all tileset sources
var source_count := tile_set.get_source_count()
# Only if an asset was added or removed
# FIXME?: may break on add+remove in 1 frame
if _cached_source_count == source_count:
return
_cached_source_count = source_count
# Process the new atlases in the TileSet
var sids := {}
for i in source_count:
var sid: int = tile_set.get_source_id(i)
if sid in _cached_sids:
sids[sid] = _cached_sids[sid]
continue
var source: TileSetSource = tile_set.get_source(sid)
if source is not TileSetAtlasSource:
push_warning(
"Non-Atlas TileSet found at index %i, source id %i.\n" % [i, source] +
"Dual Grids only support Atlas TileSets."
)
sids[sid] = null
continue
var atlas: TileSetAtlasSource = source
# FIXME?: check if this needs to be disconnected
# SETUP:
# - add logging to check which Watcher's flag was changed
# - add a TileSet with an atlas to 2 TileMapDuals
# - remove the TileSet
# - modify the terrains on one of the atlases
# - check how many watchers were flagged:
# - if 2 watchers were flagged, this is bad.
# try to repeatedly add and remove the tileset.
# this could either cause the flag to happen multiple times,
# or it could stay at 2 watchers.
# - if 1 watcher was flagged, that is ok
sids[sid] = AtlasWatcher.new(self, sid, atlas)
atlas_added.emit(sid, atlas)
_flag_terrains_changed = true
# FIXME?: find which sid's were deleted
_cached_sids = sids

View File

@@ -0,0 +1 @@
uid://bjmo6oy8a4k2k

View File

@@ -0,0 +1,62 @@
## Utility functions.
class_name Util
## Merges an Array of keys and an Array of values into a Dictionary.
static func arrays_to_dict(keys: Array, values: Array) -> Dictionary:
var out := {}
for i in keys.size():
out[keys[i]] = values[i]
return out
## Swaps the X and Y axes of a Vector2i.
static func transpose_vec(v: Vector2i) -> Vector2i:
return Vector2i(v.y, v.x)
# TODO: transposed(TileSet.CellNeighbor) -> Tileset.CellNeighbor
## Reverses the direction of a CellNeighbor.
static func reverse_neighbor(neighbor: TileSet.CellNeighbor) -> TileSet.CellNeighbor:
return (neighbor + 8) % 16
## Returns a shorthand name for a CellNeighbor.
static func neighbor_name(neighbor: TileSet.CellNeighbor) -> String:
const DIRECTIONS := ['E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'NE']
return DIRECTIONS[neighbor >> 1]
## Returns a pretty-printable neighborhood.
static func neighborhood_str(neighborhood: Array) -> String:
var neighbors := array_of(-1, 16)
for i in neighborhood.size():
neighbors[neighborhood[i]] = i
var get := func(neighbor: TileSet.CellNeighbor) -> String:
var terrain = neighbors[neighbor]
return '-' if terrain == -1 else str(terrain)
var nw = get.call(TileSet.CELL_NEIGHBOR_TOP_LEFT_CORNER)
var n = get.call(TileSet.CELL_NEIGHBOR_TOP_CORNER)
var ne = get.call(TileSet.CELL_NEIGHBOR_TOP_RIGHT_CORNER)
var w = get.call(TileSet.CELL_NEIGHBOR_LEFT_CORNER)
var e = get.call(TileSet.CELL_NEIGHBOR_RIGHT_CORNER)
var sw = get.call(TileSet.CELL_NEIGHBOR_BOTTOM_LEFT_CORNER)
var s = get.call(TileSet.CELL_NEIGHBOR_BOTTOM_CORNER)
var se = get.call(TileSet.CELL_NEIGHBOR_BOTTOM_RIGHT_CORNER)
return (
"%2s %2s %2s\n" % [nw, n, ne] +
"%2s C %2s\n" % [w, e] +
"%2s %2s %2s\n" % [sw, s, se]
)
## Returns an Array of the given size, all filled with the given value.
static func array_of(value: Variant, size: int) -> Array[Variant]:
var out := []
out.resize(size)
out.fill(value)
return out

View File

@@ -0,0 +1 @@
uid://bfbksxcjuwdjt

View File

@@ -0,0 +1,6 @@
// A shader that sets all pixels to 0 alpha, making the object invisible.
shader_type canvas_item;
void fragment() {
COLOR = vec4(0);
}

View File

@@ -0,0 +1 @@
uid://44652e0wv1ve

View File

@@ -0,0 +1,6 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cmbcfxlkxxnwq"]
[ext_resource type="Shader" uid="uid://44652e0wv1ve" path="res://addons/TileMapDual/ghost.gdshader" id="1_gvngp"]
[resource]
shader = ExtResource("1_gvngp")

View File

@@ -0,0 +1,7 @@
[plugin]
name="TileMapDual"
description="An automatic, real-time dual-grid tileset system for Godot!"
author="@GilaPixel"
version="5.0.0rc3"
script="plugin.gd"

View File

@@ -0,0 +1,63 @@
@tool
class_name TileMapDualEditorPlugin
extends EditorPlugin
static var instance: TileMapDualEditorPlugin = null
# TODO: create a message queue that groups warnings, errors, and messages into categories
# so that we don't get 300 lines of the same warnings pushed to console every time we undo/redo
func _enter_tree() -> void:
# assign singleton instance
instance = self
# register custom nodes
add_custom_type("TileMapDual", "TileMapLayer", preload("TileMapDual.gd"), preload("TileMapDual.svg"))
add_custom_type("CursorDual", "Sprite2D", preload("CursorDual.gd"), preload("CursorDual.svg"))
add_custom_type("TileMapDualLegacy", "TileMapLayer", preload("TileMapDualLegacy.gd"), preload("TileMapDual.svg"))
# load editor-only functions
TileMapDual.autotile = autotile
TileMapDual.popup = popup
# finish
print("plugin TileMapDual loaded")
func _exit_tree() -> void:
# disable editor-only functions
TileMapDual.popup = TileMapDual._editor_only.bind('popup').unbind(2)
TileMapDual.autotile = TileMapDual._editor_only.bind('autotile').unbind(3)
# remove custom nodes
remove_custom_type("TileMapDualLegacy")
remove_custom_type("CursorDual")
remove_custom_type("TileMapDual")
# unassign singleton instance
instance = null
# finish
print("plugin TileMapDual unloaded")
# HACK: functions that reference EditorPlugin, directly or indirectly, cannot be in the publicly exported scripts
# or else they simply won't work when exported
## Shows a popup with a title bar, a message, and an "Ok" button in the middle of the screen.
func popup(title: String, message: String) -> void:
var popup := AcceptDialog.new()
get_editor_interface().get_base_control().add_child(popup)
popup.name = 'TileMapDualPopup'
popup.title = title
popup.dialog_text = message
popup.popup_centered()
await popup.confirmed
popup.queue_free()
## Automatically generate terrains when the atlas is initialized.
func autotile(source_id: int, atlas: TileSetAtlasSource, tile_set: TileSet):
print_stack()
var urm := get_undo_redo()
urm.create_action("Create tiles in non-transparent texture regions", UndoRedo.MergeMode.MERGE_ALL, self, true)
# NOTE: commit_action() is called immediately after.
# NOTE: Atlas is guaranteed to have only been auto-generated with no extra peering bit information.
TerrainPreset.write_default_preset(urm, tile_set, atlas)
urm.commit_action()

View File

@@ -0,0 +1 @@
uid://irrn0ous8tet

Binary file not shown.

Binary file not shown.

BIN
aseprite/houses.aseprite Normal file

Binary file not shown.

BIN
aseprite/nature.aseprite Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://s0mghd0bccm0"
path="res://.godot/imported/monogram-extended.ttf-d0e25f0152286e72083f4de361724351.fontdata"
[deps]
source_file="res://assets/fonts/monogram-extended.ttf"
dest_files=["res://.godot/imported/monogram-extended.ttf-d0e25f0152286e72083f4de361724351.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cfsvkp6w82tgh"
path="res://.godot/imported/1bit 16px icons part-2.png-964581ea1d15a6f7999a5835abf4d89f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/textures/1bit 16px icons part-2.png"
dest_files=["res://.godot/imported/1bit 16px icons part-2.png-964581ea1d15a6f7999a5835abf4d89f.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c12v1hnrbfjr4"
path="res://.godot/imported/hood_player.png-6c2f9e5710e5416c4bf1853ba3b50627.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/textures/spritesheets/hood_player.png"
dest_files=["res://.godot/imported/hood_player.png-6c2f9e5710e5416c4bf1853ba3b50627.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://db4dctk13rgbm"
path="res://.godot/imported/houses.png-beb9348ecf7ad3cb2e053ab2dbfaa8e1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/textures/spritesheets/houses.png"
dest_files=["res://.godot/imported/houses.png-beb9348ecf7ad3cb2e053ab2dbfaa8e1.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dve2b2glwitsw"
path="res://.godot/imported/grass_tilemap.png-de5493da83e3ee84719d7aa1394438db.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/textures/tilemaps/grass_tilemap.png"
dest_files=["res://.godot/imported/grass_tilemap.png-de5493da83e3ee84719d7aa1394438db.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

View File

@@ -11,7 +11,7 @@ config_version=5
[application]
config/name="hood"
run/main_scene="uid://clnb1eshis30m"
run/main_scene="uid://dgq21ggr3br2g"
run/print_header=false
config/features=PackedStringArray("4.5", "Forward Plus")
boot_splash/bg_color=Color(0, 0, 0, 1)
@@ -22,19 +22,32 @@ config/icon="res://icon.svg"
[autoload]
Windowman="*res://scripts/autoloads/windowman.gd"
WindowManager="*res://scripts/autoloads/windowman.gd"
DialogueManager="*res://addons/dialogue_manager/dialogue_manager.gd"
Save="*res://scripts/autoloads/save.gd"
EventManager="*res://scenes/autoloads/event_manager.tscn"
Pixelize="*res://scenes/filter/pixelize.tscn"
[display]
window/stretch/mode="canvas_items"
window/stretch/scale=3.5
window/size/viewport_width=360
window/size/viewport_height=200
window/size/window_width_override=1152
window/size/window_height_override=640
window/stretch/mode="viewport"
display_server/driver.linuxbsd="wayland"
window/size/mode.release=4
[editor_plugins]
enabled=PackedStringArray("res://addons/dialogue_manager/plugin.cfg")
enabled=PackedStringArray("res://addons/TileMapDual/plugin.cfg", "res://addons/dialogue_manager/plugin.cfg")
[gui]
theme/default_font_antialiasing=0
theme/default_font_subpixel_positioning=0
theme/lcd_subpixel_layout=0
theme/custom_font="uid://s0mghd0bccm0"
[input]
@@ -87,16 +100,28 @@ escape={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":true,"script":null)
]
}
interact={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(495, 26),"global_position":Vector2(514, 122),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":89,"key_label":0,"unicode":122,"location":0,"echo":false,"script":null)
]
}
[layer_names]
2d_physics/layer_2="Interactable"
[physics]
2d/run_on_separate_thread=true
common/physics_interpolation=true
[rendering]
textures/canvas_textures/default_texture_filter=3
anti_aliasing/quality/msaa_2d=3
anti_aliasing/quality/screen_space_aa=2
anti_aliasing/quality/use_taa=true
anti_aliasing/quality/use_debanding=true
textures/canvas_textures/default_texture_filter=0
textures/default_filters/use_nearest_mipmap_filter=true
2d/snap/snap_2d_transforms_to_pixel=true
2d/snap/snap_2d_vertices_to_pixel=true
environment/defaults/default_clear_color.release=Color(0, 0, 0, 1)

View File

@@ -0,0 +1,16 @@
[gd_scene load_steps=2 format=3 uid="uid://cjbbslcx6vjjy"]
[ext_resource type="Script" uid="uid://b08q3lqbuoolb" path="res://scripts/autoloads/nodes/event_manager.gd" id="1_xplee"]
[node name="EventManager" type="CanvasLayer"]
script = ExtResource("1_xplee")
[node name="ColorRect" type="ColorRect" parent="."]
self_modulate = Color(1, 1, 1, 0)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0, 0, 0, 1)

View File

@@ -0,0 +1,23 @@
[gd_scene load_steps=3 format=3 uid="uid://dthcpgxk5b74y"]
[ext_resource type="Shader" uid="uid://dgstw8crggyti" path="res://shaders/pixelate.gdshader" id="1_tt4dp"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_83vy4"]
shader = ExtResource("1_tt4dp")
shader_parameter/texture_mode = 1
shader_parameter/quantize_size = 1
shader_parameter/snap_to_world = false
shader_parameter/limit_subpixels = true
[node name="Pixelize" type="CanvasLayer"]
[node name="ColorRect" type="ColorRect" parent="."]
visible = false
material = SubResource("ShaderMaterial_83vy4")
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
metadata/_edit_lock_ = true

27
scenes/houses/home.tscn Normal file
View File

@@ -0,0 +1,27 @@
[gd_scene load_steps=6 format=3 uid="uid://cokphmh2g8wvs"]
[ext_resource type="Texture2D" uid="uid://db4dctk13rgbm" path="res://assets/textures/spritesheets/houses.png" id="1_fltv6"]
[ext_resource type="PackedScene" uid="uid://h77hilgbces" path="res://scenes/interactables/interact_sceneswitch.tscn" id="2_0afuu"]
[ext_resource type="PackedScene" uid="uid://ccfdsdgaon63m" path="res://scenes/levels/home.tscn" id="3_0afuu"]
[sub_resource type="AtlasTexture" id="AtlasTexture_0db70"]
atlas = ExtResource("1_fltv6")
region = Rect2(1, 4, 94, 89)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_0afuu"]
size = Vector2(90, 46)
[node name="Home" type="StaticBody2D"]
[node name="Sprite2D" type="Sprite2D" parent="."]
texture = SubResource("AtlasTexture_0db70")
[node name="InteractSceneswitch" parent="." instance=ExtResource("2_0afuu")]
position = Vector2(0, 25)
scene = ExtResource("3_0afuu")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
y_sort_enabled = true
position = Vector2(0, 18)
shape = SubResource("RectangleShape2D_0afuu")
debug_color = Color(0.8768643, 0.34231007, 0.046793222, 0.41960785)

14
scenes/interactable.tscn Normal file
View File

@@ -0,0 +1,14 @@
[gd_scene load_steps=3 format=3 uid="uid://n24dhbpflcec"]
[ext_resource type="Script" uid="uid://ccthj5mtii0bw" path="res://scripts/interactable.gd" id="1_56v82"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ij5l6"]
size = Vector2(32, 32)
[node name="Interactable" type="Area2D"]
collision_layer = 2
collision_mask = 0
script = ExtResource("1_56v82")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_ij5l6")

View File

@@ -0,0 +1,11 @@
[gd_scene load_steps=3 format=3 uid="uid://h77hilgbces"]
[ext_resource type="Script" uid="uid://c4ejbchoh7yrh" path="res://scripts/interactables/interact_sceneswitch.gd" id="1_f3ssn"]
[ext_resource type="PackedScene" uid="uid://n24dhbpflcec" path="res://scenes/interactable.tscn" id="2_43o6g"]
[node name="InteractSceneswitch" type="Node2D"]
script = ExtResource("1_f3ssn")
[node name="Interactable" parent="." instance=ExtResource("2_43o6g")]
[connection signal="interacted" from="Interactable" to="." method="_on_interactable_interacted"]

84
scenes/levels/home.tscn Normal file
View File

@@ -0,0 +1,84 @@
[gd_scene load_steps=5 format=4 uid="uid://ccfdsdgaon63m"]
[ext_resource type="PackedScene" uid="uid://dfbomt0l6b1o4" path="res://scenes/player.tscn" id="1_ikf4c"]
[ext_resource type="Texture2D" uid="uid://dve2b2glwitsw" path="res://assets/textures/tilemaps/grass_tilemap.png" id="1_q28r8"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_2vl8h"]
texture = ExtResource("1_q28r8")
0:0/0 = 0
1:0/0 = 0
2:0/0 = 0
3:0/0 = 0
4:0/0 = 0
5:0/0 = 0
6:0/0 = 0
7:0/0 = 0
0:1/0 = 0
1:1/0 = 0
2:1/0 = 0
3:1/0 = 0
4:1/0 = 0
5:1/0 = 0
6:1/0 = 0
7:1/0 = 0
0:2/0 = 0
1:2/0 = 0
2:2/0 = 0
3:2/0 = 0
4:2/0 = 0
5:2/0 = 0
6:2/0 = 0
7:2/0 = 0
0:3/0 = 0
1:3/0 = 0
2:3/0 = 0
3:3/0 = 0
4:3/0 = 0
5:3/0 = 0
6:3/0 = 0
7:3/0 = 0
0:4/0 = 0
1:4/0 = 0
2:4/0 = 0
3:4/0 = 0
4:4/0 = 0
5:4/0 = 0
6:4/0 = 0
7:4/0 = 0
0:5/0 = 0
1:5/0 = 0
2:5/0 = 0
3:5/0 = 0
4:5/0 = 0
5:5/0 = 0
6:5/0 = 0
7:5/0 = 0
0:6/0 = 0
1:6/0 = 0
2:6/0 = 0
3:6/0 = 0
4:6/0 = 0
5:6/0 = 0
6:6/0 = 0
7:6/0 = 0
0:7/0 = 0
1:7/0 = 0
2:7/0 = 0
3:7/0 = 0
4:7/0 = 0
5:7/0 = 0
6:7/0 = 0
7:7/0 = 0
[sub_resource type="TileSet" id="TileSet_ikf4c"]
sources/1 = SubResource("TileSetAtlasSource_2vl8h")
[node name="Home" type="Node2D"]
[node name="TileMapLayer" type="TileMapLayer" parent="."]
tile_map_data = PackedByteArray("AAAGAAMAAQACAAMAAAAGAAQAAQACAAMAAAAGAAUAAQACAAMAAAAGAAYAAQACAAMAAAAGAAcAAQACAAMAAAAGAAgAAQACAAMAAAAGAAkAAQACAAMAAAAGAAoAAQACAAMAAAAGAAsAAQACAAMAAAAGAAwAAQACAAMAAAAHAAMAAQACAAMAAAAHAAQAAQACAAMAAAAHAAUAAQACAAMAAAAHAAYAAQACAAMAAAAHAAcAAQACAAMAAAAHAAgAAQACAAMAAAAHAAkAAQACAAMAAAAHAAoAAQACAAMAAAAHAAsAAQACAAMAAAAHAAwAAQACAAMAAAAIAAMAAQACAAMAAAAIAAQAAQACAAMAAAAIAAUAAQACAAMAAAAIAAYAAQACAAMAAAAIAAcAAQACAAMAAAAIAAgAAQACAAMAAAAIAAkAAQACAAMAAAAIAAoAAQACAAMAAAAIAAsAAQACAAMAAAAIAAwAAQACAAMAAAAJAAMAAQACAAMAAAAJAAQAAQACAAMAAAAJAAUAAQACAAMAAAAJAAYAAQACAAMAAAAJAAcAAQACAAMAAAAJAAgAAQACAAMAAAAJAAkAAQACAAMAAAAJAAoAAQACAAMAAAAJAAsAAQACAAMAAAAJAAwAAQACAAMAAAAKAAMAAQACAAMAAAAKAAQAAQACAAMAAAAKAAUAAQACAAMAAAAKAAYAAQACAAMAAAAKAAcAAQACAAMAAAAKAAgAAQACAAMAAAAKAAkAAQACAAMAAAAKAAoAAQACAAMAAAAKAAsAAQACAAMAAAAKAAwAAQACAAMAAAALAAMAAQACAAMAAAALAAQAAQACAAMAAAALAAUAAQACAAMAAAALAAYAAQACAAMAAAALAAcAAQACAAMAAAALAAgAAQACAAMAAAALAAkAAQACAAMAAAALAAoAAQACAAMAAAALAAsAAQACAAMAAAALAAwAAQACAAMAAAAMAAMAAQACAAMAAAAMAAQAAQACAAMAAAAMAAUAAQACAAMAAAAMAAYAAQACAAMAAAAMAAcAAQACAAMAAAAMAAgAAQACAAMAAAAMAAkAAQACAAMAAAAMAAoAAQACAAMAAAAMAAsAAQACAAMAAAAMAAwAAQACAAMAAAANAAMAAQACAAMAAAANAAQAAQACAAMAAAANAAUAAQACAAMAAAANAAYAAQACAAMAAAANAAcAAQACAAMAAAANAAgAAQACAAMAAAANAAkAAQACAAMAAAANAAoAAQACAAMAAAANAAsAAQACAAMAAAANAAwAAQACAAMAAAAOAAMAAQACAAMAAAAOAAQAAQACAAMAAAAOAAUAAQACAAMAAAAOAAYAAQACAAMAAAAOAAcAAQACAAMAAAAOAAgAAQACAAMAAAAOAAkAAQACAAMAAAAOAAoAAQACAAMAAAAOAAsAAQACAAMAAAAOAAwAAQACAAMAAAAPAAMAAQACAAMAAAAPAAQAAQACAAMAAAAPAAUAAQACAAMAAAAPAAYAAQACAAMAAAAPAAcAAQACAAMAAAAPAAgAAQACAAMAAAAPAAkAAQACAAMAAAAPAAoAAQACAAMAAAAPAAsAAQACAAMAAAAPAAwAAQACAAMAAAA=")
tile_set = SubResource("TileSet_ikf4c")
[node name="Player" parent="." node_paths=PackedStringArray("tilemap") instance=ExtResource("1_ikf4c")]
position = Vector2(180, 183)
tilemap = NodePath("../TileMapLayer")

View File

@@ -1,10 +1,225 @@
[gd_scene load_steps=2 format=3 uid="uid://clnb1eshis30m"]
[gd_scene load_steps=8 format=4 uid="uid://clnb1eshis30m"]
[ext_resource type="Texture2D" uid="uid://dve2b2glwitsw" path="res://assets/textures/tilemaps/grass_tilemap.png" id="1_74em3"]
[ext_resource type="PackedScene" uid="uid://dfbomt0l6b1o4" path="res://scenes/player.tscn" id="1_ge1l5"]
[ext_resource type="PackedScene" uid="uid://cokphmh2g8wvs" path="res://scenes/houses/home.tscn" id="3_5rqdi"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_1r5ec"]
texture = ExtResource("1_74em3")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/terrain_set = 0
0:0/0/terrain = 0
0:0/0/terrains_peering_bit/right_side = 0
0:0/0/terrains_peering_bit/bottom_right_corner = 0
0:0/0/terrains_peering_bit/bottom_side = 0
0:0/0/terrains_peering_bit/bottom_left_corner = 1
0:0/0/terrains_peering_bit/left_side = 0
0:0/0/terrains_peering_bit/top_left_corner = 0
0:0/0/terrains_peering_bit/top_side = 0
0:0/0/terrains_peering_bit/top_right_corner = 0
1:0/0 = 0
1:0/0/terrain_set = 0
1:0/0/terrain = 0
1:0/0/terrains_peering_bit/right_side = 1
1:0/0/terrains_peering_bit/bottom_right_corner = 1
1:0/0/terrains_peering_bit/bottom_side = 0
1:0/0/terrains_peering_bit/bottom_left_corner = 0
1:0/0/terrains_peering_bit/left_side = 0
1:0/0/terrains_peering_bit/top_left_corner = 0
1:0/0/terrains_peering_bit/top_side = 0
1:0/0/terrains_peering_bit/top_right_corner = 1
2:0/0 = 0
2:0/0/terrain_set = 0
2:0/0/terrain = 0
2:0/0/terrains_peering_bit/right_side = 0
2:0/0/terrains_peering_bit/bottom_right_corner = 1
2:0/0/terrains_peering_bit/bottom_side = 1
2:0/0/terrains_peering_bit/bottom_left_corner = 1
2:0/0/terrains_peering_bit/left_side = 1
2:0/0/terrains_peering_bit/top_left_corner = 1
2:0/0/terrains_peering_bit/top_side = 0
2:0/0/terrains_peering_bit/top_right_corner = 0
3:0/0 = 0
3:0/0/terrain_set = 0
3:0/0/terrain = 0
3:0/0/terrains_peering_bit/right_side = 0
3:0/0/terrains_peering_bit/bottom_right_corner = 1
3:0/0/terrains_peering_bit/bottom_side = 1
3:0/0/terrains_peering_bit/bottom_left_corner = 1
3:0/0/terrains_peering_bit/left_side = 0
3:0/0/terrains_peering_bit/top_left_corner = 0
3:0/0/terrains_peering_bit/top_side = 0
3:0/0/terrains_peering_bit/top_right_corner = 0
0:1/0 = 0
0:1/0/terrain_set = 0
0:1/0/terrain = 0
0:1/0/terrains_peering_bit/right_side = 0
0:1/0/terrains_peering_bit/bottom_right_corner = 1
0:1/0/terrains_peering_bit/bottom_side = 0
0:1/0/terrains_peering_bit/bottom_left_corner = 0
0:1/0/terrains_peering_bit/left_side = 0
0:1/0/terrains_peering_bit/top_left_corner = 1
0:1/0/terrains_peering_bit/top_side = 0
0:1/0/terrains_peering_bit/top_right_corner = 0
1:1/0 = 0
1:1/0/terrain_set = 0
1:1/0/terrain = 0
1:1/0/terrains_peering_bit/right_side = 1
1:1/0/terrains_peering_bit/bottom_right_corner = 1
1:1/0/terrains_peering_bit/bottom_side = 1
1:1/0/terrains_peering_bit/bottom_left_corner = 1
1:1/0/terrains_peering_bit/left_side = 0
1:1/0/terrains_peering_bit/top_left_corner = 0
1:1/0/terrains_peering_bit/top_side = 0
1:1/0/terrains_peering_bit/top_right_corner = 1
2:1/0 = 0
2:1/0/terrain_set = 0
2:1/0/terrain = 1
2:1/0/terrains_peering_bit/right_side = 1
2:1/0/terrains_peering_bit/bottom_right_corner = 1
2:1/0/terrains_peering_bit/bottom_side = 1
2:1/0/terrains_peering_bit/bottom_left_corner = 1
2:1/0/terrains_peering_bit/left_side = 1
2:1/0/terrains_peering_bit/top_left_corner = 1
2:1/0/terrains_peering_bit/top_side = 1
2:1/0/terrains_peering_bit/top_right_corner = 1
3:1/0 = 0
3:1/0/terrain_set = 0
3:1/0/terrain = 0
3:1/0/terrains_peering_bit/right_side = 0
3:1/0/terrains_peering_bit/bottom_right_corner = 0
3:1/0/terrains_peering_bit/bottom_side = 0
3:1/0/terrains_peering_bit/bottom_left_corner = 1
3:1/0/terrains_peering_bit/left_side = 1
3:1/0/terrains_peering_bit/top_left_corner = 1
3:1/0/terrains_peering_bit/top_side = 1
3:1/0/terrains_peering_bit/top_right_corner = 1
0:2/0 = 0
0:2/0/terrain_set = 0
0:2/0/terrain = 0
0:2/0/terrains_peering_bit/right_side = 0
0:2/0/terrains_peering_bit/bottom_right_corner = 0
0:2/0/terrains_peering_bit/bottom_side = 0
0:2/0/terrains_peering_bit/bottom_left_corner = 0
0:2/0/terrains_peering_bit/left_side = 0
0:2/0/terrains_peering_bit/top_left_corner = 0
0:2/0/terrains_peering_bit/top_side = 0
0:2/0/terrains_peering_bit/top_right_corner = 1
1:2/0 = 0
1:2/0/terrain_set = 0
1:2/0/terrain = 0
1:2/0/terrains_peering_bit/right_side = 0
1:2/0/terrains_peering_bit/bottom_right_corner = 0
1:2/0/terrains_peering_bit/bottom_side = 0
1:2/0/terrains_peering_bit/bottom_left_corner = 0
1:2/0/terrains_peering_bit/left_side = 0
1:2/0/terrains_peering_bit/top_left_corner = 1
1:2/0/terrains_peering_bit/top_side = 1
1:2/0/terrains_peering_bit/top_right_corner = 1
2:2/0 = 0
2:2/0/terrain_set = 0
2:2/0/terrain = 0
2:2/0/terrains_peering_bit/right_side = 1
2:2/0/terrains_peering_bit/bottom_right_corner = 1
2:2/0/terrains_peering_bit/bottom_side = 0
2:2/0/terrains_peering_bit/bottom_left_corner = 0
2:2/0/terrains_peering_bit/left_side = 0
2:2/0/terrains_peering_bit/top_left_corner = 1
2:2/0/terrains_peering_bit/top_side = 1
2:2/0/terrains_peering_bit/top_right_corner = 1
3:2/0 = 0
3:2/0/terrain_set = 0
3:2/0/terrain = 0
3:2/0/terrains_peering_bit/right_side = 0
3:2/0/terrains_peering_bit/bottom_right_corner = 0
3:2/0/terrains_peering_bit/bottom_side = 0
3:2/0/terrains_peering_bit/bottom_left_corner = 1
3:2/0/terrains_peering_bit/left_side = 1
3:2/0/terrains_peering_bit/top_left_corner = 1
3:2/0/terrains_peering_bit/top_side = 0
3:2/0/terrains_peering_bit/top_right_corner = 0
0:3/0 = 0
0:3/0/terrain_set = 0
0:3/0/terrain = 0
0:3/0/terrains_peering_bit/right_side = 0
0:3/0/terrains_peering_bit/bottom_right_corner = 0
0:3/0/terrains_peering_bit/bottom_side = 0
0:3/0/terrains_peering_bit/bottom_left_corner = 0
0:3/0/terrains_peering_bit/left_side = 0
0:3/0/terrains_peering_bit/top_left_corner = 0
0:3/0/terrains_peering_bit/top_side = 0
0:3/0/terrains_peering_bit/top_right_corner = 0
1:3/0 = 0
1:3/0/terrain_set = 0
1:3/0/terrain = 0
1:3/0/terrains_peering_bit/right_side = 0
1:3/0/terrains_peering_bit/bottom_right_corner = 1
1:3/0/terrains_peering_bit/bottom_side = 0
1:3/0/terrains_peering_bit/bottom_left_corner = 0
1:3/0/terrains_peering_bit/left_side = 0
1:3/0/terrains_peering_bit/top_left_corner = 0
1:3/0/terrains_peering_bit/top_side = 0
1:3/0/terrains_peering_bit/top_right_corner = 0
2:3/0 = 0
2:3/0/terrain_set = 0
2:3/0/terrain = 0
2:3/0/terrains_peering_bit/right_side = 0
2:3/0/terrains_peering_bit/bottom_right_corner = 0
2:3/0/terrains_peering_bit/bottom_side = 0
2:3/0/terrains_peering_bit/bottom_left_corner = 1
2:3/0/terrains_peering_bit/left_side = 0
2:3/0/terrains_peering_bit/top_left_corner = 0
2:3/0/terrains_peering_bit/top_side = 0
2:3/0/terrains_peering_bit/top_right_corner = 1
3:3/0 = 0
3:3/0/terrain_set = 0
3:3/0/terrain = 0
3:3/0/terrains_peering_bit/right_side = 0
3:3/0/terrains_peering_bit/bottom_right_corner = 0
3:3/0/terrains_peering_bit/bottom_side = 0
3:3/0/terrains_peering_bit/bottom_left_corner = 0
3:3/0/terrains_peering_bit/left_side = 0
3:3/0/terrains_peering_bit/top_left_corner = 1
3:3/0/terrains_peering_bit/top_side = 0
3:3/0/terrains_peering_bit/top_right_corner = 0
[sub_resource type="TileSet" id="TileSet_ge1l5"]
tile_size = Vector2i(32, 32)
terrain_set_0/mode = 0
terrain_set_0/terrain_0/name = "Grass"
terrain_set_0/terrain_0/color = Color(0, 0.5365651, 0.20230255, 1)
terrain_set_0/terrain_1/name = "Dirt"
terrain_set_0/terrain_1/color = Color(0.5, 0.34375, 0.25, 1)
sources/2 = SubResource("TileSetAtlasSource_1r5ec")
[sub_resource type="TileSetScenesCollectionSource" id="TileSetScenesCollectionSource_1r5ec"]
resource_name = "Houses"
scenes/1/scene = ExtResource("3_5rqdi")
[sub_resource type="TileSet" id="TileSet_5rqdi"]
tile_size = Vector2i(32, 32)
sources/0 = SubResource("TileSetScenesCollectionSource_1r5ec")
[node name="Hood" type="Node2D"]
z_as_relative = false
y_sort_enabled = true
[node name="TileMapLayer" type="TileMapLayer" parent="."]
[node name="Ground" type="TileMapLayer" parent="."]
z_index = -1
tile_map_data = PackedByteArray("AAAQAAkAAgAAAAMAAAAQAAgAAgAAAAMAAAAQAAcAAgAAAAMAAAAQAAYAAgAAAAMAAAAQAAUAAgAAAAMAAAAQAAQAAgAAAAMAAAAQAAMAAgAAAAMAAAAQAAIAAgAAAAMAAAAQAAEAAgAAAAMAAAAQAAAAAgAAAAMAAAAQAP//AgAAAAMAAAAQAP7/AgAAAAMAAAAPAAkAAgAAAAMAAAAPAAgAAgAAAAMAAAAPAAcAAgAAAAMAAAAPAAYAAgADAAMAAAAPAAUAAgADAAIAAAAPAAQAAgAAAAAAAAAPAAMAAgAAAAMAAAAPAAIAAgAAAAMAAAAPAAEAAgAAAAMAAAAPAAAAAgAAAAMAAAAPAP//AgAAAAMAAAAPAP7/AgAAAAMAAAAOAAkAAgAAAAMAAAAOAAgAAgAAAAMAAAAOAAcAAgAAAAMAAAAOAAYAAgABAAIAAAAOAAUAAgACAAEAAAAOAAQAAgADAAAAAAAOAAMAAgAAAAMAAAAOAAIAAgAAAAMAAAAOAAEAAgAAAAMAAAAOAAAAAgAAAAMAAAAOAP//AgAAAAMAAAAOAP7/AgAAAAMAAAANAAkAAgAAAAMAAAANAAgAAgAAAAMAAAANAAcAAgAAAAMAAAANAAYAAgABAAIAAAANAAUAAgACAAEAAAANAAQAAgADAAAAAAANAAMAAgAAAAMAAAANAAIAAgAAAAMAAAANAAEAAgAAAAMAAAANAAAAAgAAAAMAAAANAP//AgAAAAMAAAANAP7/AgAAAAMAAAAMAAkAAgAAAAMAAAAMAAgAAgAAAAMAAAAMAAcAAgAAAAMAAAAMAAYAAgABAAIAAAAMAAUAAgACAAEAAAAMAAQAAgADAAAAAAAMAAMAAgAAAAMAAAAMAAIAAgAAAAMAAAAMAAEAAgAAAAMAAAAMAAAAAgAAAAMAAAAMAP//AgAAAAMAAAAMAP7/AgAAAAMAAAALAAkAAgAAAAMAAAALAAgAAgAAAAMAAAALAAcAAgAAAAMAAAALAAYAAgABAAIAAAALAAUAAgACAAEAAAALAAQAAgADAAAAAAALAAMAAgAAAAMAAAALAAIAAgAAAAMAAAALAAEAAgAAAAMAAAALAAAAAgAAAAMAAAALAP//AgAAAAMAAAALAP7/AgAAAAMAAAAKAAkAAgAAAAMAAAAKAAgAAgAAAAMAAAAKAAcAAgAAAAMAAAAKAAYAAgABAAIAAAAKAAUAAgACAAEAAAAKAAQAAgACAAAAAAAKAAMAAgADAAIAAAAKAAIAAgAAAAAAAAAKAAEAAgAAAAMAAAAKAAAAAgAAAAMAAAAKAP//AgAAAAMAAAAKAP7/AgAAAAMAAAAJAAkAAgAAAAMAAAAJAAgAAgAAAAMAAAAJAAcAAgAAAAMAAAAJAAYAAgABAAIAAAAJAAUAAgACAAEAAAAJAAQAAgACAAEAAAAJAAMAAgACAAEAAAAJAAIAAgADAAAAAAAJAAEAAgAAAAMAAAAJAAAAAgAAAAMAAAAJAP//AgAAAAMAAAAJAP7/AgAAAAMAAAAIAAkAAgAAAAMAAAAIAAgAAgAAAAMAAAAIAAcAAgAAAAMAAAAIAAYAAgABAAIAAAAIAAUAAgACAAEAAAAIAAQAAgABAAEAAAAIAAMAAgABAAAAAAAIAAIAAgABAAMAAAAIAAEAAgAAAAMAAAAIAAAAAgAAAAMAAAAIAP//AgAAAAMAAAAIAP7/AgAAAAMAAAAHAAkAAgAAAAMAAAAHAAgAAgAAAAMAAAAHAAcAAgAAAAMAAAAHAAYAAgABAAIAAAAHAAUAAgACAAEAAAAHAAQAAgADAAAAAAAHAAMAAgAAAAMAAAAHAAIAAgAAAAMAAAAHAAEAAgAAAAMAAAAHAAAAAgAAAAMAAAAHAP//AgAAAAMAAAAHAP7/AgAAAAMAAAAGAAkAAgAAAAMAAAAGAAgAAgAAAAMAAAAGAAcAAgAAAAMAAAAGAAYAAgABAAIAAAAGAAUAAgACAAEAAAAGAAQAAgADAAAAAAAGAAMAAgAAAAMAAAAGAAIAAgAAAAMAAAAGAAEAAgAAAAMAAAAGAAAAAgAAAAMAAAAGAP//AgAAAAMAAAAGAP7/AgAAAAMAAAAFAAkAAgAAAAMAAAAFAAgAAgAAAAMAAAAFAAcAAgAAAAMAAAAFAAYAAgABAAIAAAAFAAUAAgACAAEAAAAFAAQAAgADAAAAAAAFAAMAAgAAAAMAAAAFAAIAAgAAAAMAAAAFAAEAAgAAAAMAAAAFAAAAAgAAAAMAAAAFAP//AgAAAAMAAAAFAP7/AgAAAAMAAAAEAAkAAgAAAAMAAAAEAAgAAgAAAAMAAAAEAAcAAgAAAAMAAAAEAAYAAgABAAIAAAAEAAUAAgACAAEAAAAEAAQAAgACAAAAAAAEAAMAAgADAAIAAAAEAAIAAgADAAIAAAAEAAEAAgAAAAAAAAAEAAAAAgAAAAMAAAAEAP//AgAAAAMAAAAEAP7/AgAAAAMAAAADAAkAAgAAAAMAAAADAAgAAgAAAAMAAAADAAcAAgAAAAMAAAADAAYAAgABAAIAAAADAAUAAgACAAEAAAADAAQAAgACAAEAAAADAAMAAgACAAEAAAADAAIAAgACAAEAAAADAAEAAgADAAAAAAADAAAAAgAAAAMAAAADAP//AgAAAAMAAAADAP7/AgAAAAMAAAACAAkAAgAAAAMAAAACAAgAAgAAAAMAAAACAAcAAgAAAAMAAAACAAYAAgAAAAIAAAACAAUAAgABAAAAAAACAAQAAgACAAIAAAACAAMAAgACAAEAAAACAAIAAgABAAEAAAACAAEAAgABAAMAAAACAAAAAgAAAAMAAAACAP//AgAAAAMAAAACAP7/AgAAAAMAAAABAAkAAgAAAAMAAAABAAgAAgAAAAMAAAABAAcAAgAAAAMAAAABAAYAAgAAAAMAAAABAAUAAgAAAAMAAAABAAQAAgABAAIAAAABAAMAAgACAAEAAAABAAIAAgADAAAAAAABAAEAAgAAAAMAAAABAAAAAgAAAAMAAAABAP//AgAAAAMAAAABAP7/AgAAAAMAAAAAAAkAAgAAAAMAAAAAAAgAAgAAAAMAAAAAAAcAAgAAAAMAAAAAAAYAAgAAAAMAAAAAAAUAAgAAAAMAAAAAAAQAAgABAAIAAAAAAAMAAgACAAEAAAAAAAIAAgADAAAAAAAAAAEAAgAAAAMAAAAAAAAAAgAAAAMAAAAAAP//AgAAAAMAAAAAAP7/AgAAAAMAAAD//wkAAgAAAAMAAAD//wgAAgAAAAMAAAD//wcAAgAAAAMAAAD//wYAAgAAAAMAAAD//wUAAgAAAAMAAAD//wQAAgABAAIAAAD//wMAAgACAAEAAAD//wIAAgADAAAAAAD//wEAAgAAAAMAAAD//wAAAgAAAAMAAAD/////AgAAAAMAAAD///7/AgAAAAMAAAD+/wkAAgAAAAMAAAD+/wgAAgAAAAMAAAD+/wcAAgAAAAMAAAD+/wYAAgAAAAMAAAD+/wUAAgAAAAMAAAD+/wQAAgAAAAIAAAD+/wMAAgABAAAAAAD+/wIAAgABAAMAAAD+/wEAAgAAAAMAAAD+/wAAAgAAAAMAAAD+////AgAAAAMAAAD+//7/AgAAAAMAAAA=")
tile_set = SubResource("TileSet_ge1l5")
rendering_quadrant_size = 32
physics_quadrant_size = 32
metadata/_edit_lock_ = true
[node name="Player" parent="." instance=ExtResource("1_ge1l5")]
position = Vector2(128, 76)
[node name="Objects" type="TileMapLayer" parent="."]
y_sort_enabled = true
tile_map_data = PackedByteArray("AAADAAAAAAAAAAAAAQA=")
tile_set = SubResource("TileSet_5rqdi")
rendering_quadrant_size = 32
physics_quadrant_size = 32
[node name="Player" parent="." node_paths=PackedStringArray("tilemap") instance=ExtResource("1_ge1l5")]
position = Vector2(110, 38)
tilemap = NodePath("../Ground")

View File

@@ -0,0 +1,54 @@
[gd_scene load_steps=3 format=3 uid="uid://dgq21ggr3br2g"]
[ext_resource type="Script" uid="uid://bfgqkpn60hhwj" path="res://scripts/menus/main_menu.gd" id="1_xqga4"]
[ext_resource type="PackedScene" uid="uid://cd657k2lc3gyc" path="res://scenes/menus/profile_creator.tscn" id="2_ovrgc"]
[node name="MainMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_xqga4")
[node name="TabContainer" type="TabContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
current_tab = 0
tabs_visible = false
drag_to_rearrange_enabled = true
[node name="Main" type="Control" parent="TabContainer"]
layout_mode = 2
metadata/_tab_index = 0
[node name="VBoxContainer" type="VBoxContainer" parent="TabContainer/Main"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -20.0
offset_right = 20.0
offset_bottom = 20.0
grow_horizontal = 2
grow_vertical = 2
[node name="Button" type="Button" parent="TabContainer/Main/VBoxContainer"]
layout_mode = 2
text = "Start"
[node name="ProfileCreator" parent="TabContainer" instance=ExtResource("2_ovrgc")]
visible = false
layout_mode = 2
metadata/_tab_index = 1
[connection signal="pressed" from="TabContainer/Main/VBoxContainer/Button" to="." method="_on_start_button_pressed"]
[connection signal="exit" from="TabContainer/ProfileCreator" to="." method="_on_profile_creator_exit"]

View File

@@ -0,0 +1,58 @@
[gd_scene load_steps=3 format=3 uid="uid://cd657k2lc3gyc"]
[ext_resource type="Script" uid="uid://cs1ylnivyrbc2" path="res://scripts/menus/profile_creator.gd" id="1_4xjd5"]
[ext_resource type="FontFile" uid="uid://s0mghd0bccm0" path="res://assets/fonts/monogram-extended.ttf" id="2_yd267"]
[node name="ProfileCreator" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_4xjd5")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -74.0
offset_top = -21.5
offset_right = 74.0
offset_bottom = 21.5
grow_horizontal = 2
grow_vertical = 2
[node name="LineEdit" type="LineEdit" parent="VBoxContainer"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_yd267")
placeholder_text = "Name him"
editable = false
context_menu_enabled = false
emoji_menu_enabled = false
virtual_keyboard_enabled = false
virtual_keyboard_show_on_focus = false
clear_button_enabled = true
selecting_enabled = false
caret_blink = true
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Back" type="Button" parent="VBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
text = "Back"
[node name="Next" type="Button" parent="VBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 4
text = "Next"
[connection signal="pressed" from="VBoxContainer/HBoxContainer/Back" to="." method="_on_back_button_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/Next" to="." method="_on_next_pressed"]

View File

@@ -0,0 +1,191 @@
[gd_scene load_steps=4 format=3 uid="uid://dn0lr1sbir3d3"]
[ext_resource type="Script" uid="uid://bxquk4wo56r22" path="res://scripts/menus/util/keyboard.gd" id="1_xrtsm"]
[ext_resource type="Texture2D" uid="uid://cfsvkp6w82tgh" path="res://assets/textures/1bit 16px icons part-2.png" id="2_v4hro"]
[sub_resource type="AtlasTexture" id="AtlasTexture_l3m3g"]
atlas = ExtResource("2_v4hro")
region = Rect2(296.5, 82, 13, 13)
[node name="Keyboard" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 3.0
offset_top = 1.0
offset_right = 3.0
offset_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 4
size_flags_vertical = 4
script = ExtResource("1_xrtsm")
[node name="TextLabel" type="Label" parent="."]
layout_mode = 1
anchors_preset = -1
anchor_left = 0.5
anchor_top = 0.202
anchor_right = 0.5
anchor_bottom = 0.202
offset_left = -125.0
offset_top = -6.4000015
offset_right = 126.0
offset_bottom = 6.5999985
grow_horizontal = 2
[node name="MainKeys" type="HFlowContainer" parent="."]
custom_minimum_size = Vector2(230, 100)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -115.0
offset_top = -50.5
offset_right = 154.0
offset_bottom = 50.5
grow_horizontal = 2
grow_vertical = 2
[node name="Button" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "A"
[node name="Button2" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "B"
[node name="Button3" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "C"
[node name="Button4" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "D"
[node name="Button5" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "E"
[node name="Button6" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "F"
[node name="Button7" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "G"
[node name="Button8" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "H"
[node name="Button9" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "I"
[node name="UShift" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
icon = SubResource("AtlasTexture_l3m3g")
[node name="Button10" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "J"
[node name="Button11" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "K"
[node name="Button12" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "L"
[node name="Button13" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "M"
[node name="Button14" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "N"
[node name="Button15" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "O"
[node name="Button16" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "P"
[node name="Button17" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "Q"
[node name="Button18" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "R"
[node name="Button19" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "S"
[node name="Button20" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "T"
[node name="Button21" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "U"
[node name="Button22" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "V"
[node name="Button23" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "W"
[node name="USpace" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(74, 0)
layout_mode = 2
text = "Space"
[node name="Button24" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "X"
[node name="Button25" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "Y"
[node name="Button26" type="Button" parent="MainKeys"]
custom_minimum_size = Vector2(22, 22)
layout_mode = 2
text = "Z"

View File

@@ -1,17 +1,96 @@
[gd_scene load_steps=5 format=3 uid="uid://dfbomt0l6b1o4"]
[gd_scene load_steps=14 format=3 uid="uid://dfbomt0l6b1o4"]
[ext_resource type="Script" uid="uid://dxvslwwnnlosy" path="res://scripts/player.gd" id="1_3vyb7"]
[ext_resource type="Texture2D" uid="uid://bivyvi585d2lk" path="res://assets/textures/hood_player.png" id="2_g2els"]
[ext_resource type="Texture2D" uid="uid://c12v1hnrbfjr4" path="res://assets/textures/spritesheets/hood_player.png" id="2_g2els"]
[sub_resource type="AtlasTexture" id="AtlasTexture_g2els"]
atlas = ExtResource("2_g2els")
region = Rect2(0, 0, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_qhqgy"]
atlas = ExtResource("2_g2els")
region = Rect2(34, 0, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_dqkch"]
atlas = ExtResource("2_g2els")
region = Rect2(68, 0, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_qlg0r"]
atlas = ExtResource("2_g2els")
region = Rect2(0, 34, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_tuyoq"]
atlas = ExtResource("2_g2els")
region = Rect2(34, 34, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_fjrip"]
atlas = ExtResource("2_g2els")
region = Rect2(68, 34, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_smehm"]
atlas = ExtResource("2_g2els")
region = Rect2(0, 68, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_ur7pv"]
atlas = ExtResource("2_g2els")
region = Rect2(34, 68, 33, 33)
[sub_resource type="AtlasTexture" id="AtlasTexture_y4r1p"]
atlas = ExtResource("2_g2els")
region = Rect2(68, 68, 33, 33)
[sub_resource type="SpriteFrames" id="SpriteFrames_qhqgy"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": ExtResource("2_g2els")
"texture": SubResource("AtlasTexture_g2els")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_qhqgy")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_g2els")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_dqkch")
}],
"loop": true,
"name": &"default",
"speed": 5.0
"name": &"down",
"speed": 6.5
}, {
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_qlg0r")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_tuyoq")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_qlg0r")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_fjrip")
}],
"loop": true,
"name": &"side",
"speed": 6.0
}, {
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_smehm")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ur7pv")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_smehm")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_y4r1p")
}],
"loop": true,
"name": &"up",
"speed": 6.5
}]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_3vyb7"]
@@ -19,12 +98,27 @@ radius = 3.0
height = 14.0
[node name="Player" type="CharacterBody2D"]
motion_mode = 1
script = ExtResource("1_3vyb7")
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."]
y_sort_enabled = true
sprite_frames = SubResource("SpriteFrames_qhqgy")
animation = &"down"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, 13)
rotation = -1.5707964
shape = SubResource("CapsuleShape2D_3vyb7")
[node name="Camera2D" type="Camera2D" parent="."]
process_callback = 0
drag_horizontal_enabled = true
drag_vertical_enabled = true
[node name="RayCast2D" type="RayCast2D" parent="."]
target_position = Vector2(0, 32)
collision_mask = 2
hit_from_inside = true
collide_with_areas = true
collide_with_bodies = false

View File

@@ -0,0 +1,24 @@
extends Node
var player_free: bool = true
var player: Player
enum Events { NONE, PUMPKIN_CARVE, OUTSIDE_NORMAL }
var current_event: Events = Events.NONE
func transition_start() -> void:
pass
func transition_end() -> void:
pass
func run_event(event: Events, player_postion: Vector2 = Vector2.ZERO):
current_event = event
transition_start()
if player != null && player_postion != Vector2.ZERO:
player.position = player_postion
match event:
Events.PUMPKIN_CARVE:
get_tree().change_scene_to_file("uid://ccfdsdgaon63m") # scenes/levels/home.tscn
pass
transition_end()

View File

@@ -0,0 +1 @@
uid://b08q3lqbuoolb

44
scripts/autoloads/save.gd Normal file
View File

@@ -0,0 +1,44 @@
class_name SaveManager
extends Node
var file: ConfigFile = ConfigFile.new()
var save_path: String = "user://save.binary"
func save(res: SaveFile) -> bool:
if not is_instance_valid(res):
push_error("cannot save: invalid resource")
return false
if FileAccess.file_exists(save_path) and file.load(save_path) != OK:
printerr("Save file corrupted!")
return false
file.set_value("meta", "savedate", Time.get_datetime_string_from_system())
for property in res.get_property_list():
if property.usage & PROPERTY_USAGE_STORAGE:
var v_name: String = property.name
file.set_value(res.save_name, v_name, res.get(v_name))
return file.save(save_path) == OK
func load(res: SaveFile) -> bool:
if not FileAccess.file_exists(save_path):
return true
if file.load(save_path) != OK:
printerr("Save file corrupted!")
return false
var props := []
for p in res.get_property_list():
if p.usage & PROPERTY_USAGE_STORAGE:
props.append(p.name)
for key in file.get_section_keys(res.save_name):
if key in props:
res.set(key, file.get_value(res.save_name, key))
return true
func delete() -> void:
file.clear()
file.save(save_path)

View File

@@ -0,0 +1 @@
uid://dfgqnmfb710g8

View File

@@ -1,10 +1,15 @@
extends Node
@onready var fullscreen: bool = ProjectSettings.get_setting("display/window/size/mode") == Window.MODE_EXCLUSIVE_FULLSCREEN | Window.MODE_FULLSCREEN
var config: Config = Config.new("config")
func _ready() -> void:
config.load()
toggle_fullscreen()
func _process(_delta: float) -> void:
if Input.is_action_just_pressed("fullscreen"):
fullscreen = !fullscreen
config.fullscreen = !config.fullscreen
config.save()
toggle_fullscreen()
func _notification(what: int) -> void:
@@ -18,7 +23,7 @@ func _unhandled_input(event: InputEvent) -> void:
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
func toggle_fullscreen() -> void:
if fullscreen:
if config.fullscreen:
if OS.get_name() == "Windows":
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN)
else:

6
scripts/interactable.gd Normal file
View File

@@ -0,0 +1,6 @@
class_name Interactable
extends Area2D
signal interacted
func interact() -> void: interacted.emit()

View File

@@ -0,0 +1 @@
uid://ccthj5mtii0bw

View File

@@ -0,0 +1,6 @@
extends Node2D
@export var scene: PackedScene
func _on_interactable_interacted() -> void:
get_tree().change_scene_to_packed(scene)

View File

@@ -0,0 +1 @@
uid://c4ejbchoh7yrh

View File

@@ -0,0 +1,9 @@
extends Control
@onready var tab_container: TabContainer = $TabContainer
func _on_start_button_pressed() -> void:
tab_container.current_tab = 1
func _on_profile_creator_exit() -> void:
tab_container.current_tab = 0

View File

@@ -0,0 +1 @@
uid://bfgqkpn60hhwj

View File

@@ -0,0 +1,45 @@
extends Control
signal exit
var profile: Profile = Profile.new("profile")
@onready var next: Button = $VBoxContainer/HBoxContainer/Next
@onready var line_edit: LineEdit = $VBoxContainer/LineEdit
@onready var max_index: int = profile.named_things.keys().size() - 1
var index: int = 0
func _ready() -> void:
next.disabled = index == max_index
func _on_back_button_pressed() -> void:
if index == 0:
exit.emit()
else:
index -= 1
update_name()
func _on_next_pressed() -> void:
index += 1
update_name()
func update_name() -> void:
if index == max_index:
next.text = "Finish"
elif index > max_index:
profile.save()
EventManager.run_event(EventManager.Events.PUMPKIN_CARVE)
return
else:
next.text = "Next"
var labels: Dictionary = {
"player_name": "Name him",
"villain_friend": "Name him too",
"stupid_friend": "And also name him",
"favourite_candy": "Favourite candy?"
}
var label: String = labels[profile.named_things.keys()[index]]
line_edit.placeholder_text = label

View File

@@ -0,0 +1 @@
uid://cs1ylnivyrbc2

View File

@@ -0,0 +1,33 @@
extends Control
signal text_changed
signal key_pressed
@onready var main_keys: HFlowContainer = $MainKeys
@onready var text_label: Label = $TextLabel
@export var text: String = "":
set(value):
text = value
text_label.text = text
text_changed.emit()
var shifting: bool = true
func _ready() -> void:
for key: Button in main_keys.get_children():
key.connect("pressed", func():
key_pressed.emit()
match key.name:
"UShift":
shifting = !shifting
for i_key: Button in main_keys.get_children():
if shifting:
i_key.text = i_key.text.to_upper()
else:
i_key.text = i_key.text.to_lower()
"USpace":
text += " "
_:
text += key.text
)

View File

@@ -0,0 +1 @@
uid://bxquk4wo56r22

View File

@@ -1,7 +1,69 @@
class_name Player
extends CharacterBody2D
const SPEED: int = 500
const SPEED: int = 800
@onready var animated_sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var cam: Camera2D = $Camera2D
@onready var raycast: RayCast2D = $RayCast2D
const RAYCAST_RAGE: int = 32
@export var tilemap: TileMapLayer
var tilesize: int = 32
var position_limit_rect: Rect2
func _ready() -> void:
EventManager.player = self
var used_tilemap_rect: Rect2i = tilemap.get_used_rect()
tilesize = tilemap.tile_set.tile_size.x
cam.limit_left = used_tilemap_rect.position.x * tilesize
cam.limit_top = used_tilemap_rect.position.y * tilesize
cam.limit_right = used_tilemap_rect.end.x * tilesize
cam.limit_bottom = used_tilemap_rect.end.y * tilesize
var margin: float = float(tilesize) / 4
position_limit_rect = Rect2(
Vector2(cam.limit_left + margin, cam.limit_top - margin),
Vector2(
(cam.limit_right - margin) - (cam.limit_left + margin),
(cam.limit_bottom - margin / 2) - (cam.limit_top + margin / 2)
)
)
func _process(_delta: float) -> void:
if Input.is_action_just_pressed("interact") && raycast.is_colliding() && raycast.get_collider() is Interactable:
var interactable: Interactable = raycast.get_collider()
interactable.interact()
func _physics_process(delta: float) -> void:
velocity = Input.get_vector("move_left","move_right","move_up","move_down") * delta * SPEED * 3
if EventManager.player_free:
var input := Input.get_vector("move_left", "move_right", "move_up", "move_down").normalized()
var raycast_input := Vector2.ZERO
if abs(input.x) > abs(input.y):
raycast_input.x = sign(input.x)
elif abs(input.y) > 0:
raycast_input.y = sign(input.y)
if raycast_input != Vector2.ZERO:
raycast.target_position = raycast_input * RAYCAST_RAGE
velocity = input * delta * SPEED * 3
move_and_slide()
position = position.clamp(position_limit_rect.position, position_limit_rect.end)
if velocity.length() != 0:
animated_sprite.play()
animated_sprite.flip_h = false
if abs(velocity.x) > abs(velocity.y):
animated_sprite.animation = "side"
animated_sprite.flip_h = velocity.x < 0
elif velocity.y < 0:
animated_sprite.animation = "up"
else:
animated_sprite.animation = "down"
else:
animated_sprite.stop()

View File

@@ -0,0 +1,4 @@
class_name Config
extends SaveFile
@export var fullscreen: bool = ProjectSettings.get_setting("display/window/size/mode") == Window.MODE_EXCLUSIVE_FULLSCREEN | Window.MODE_FULLSCREEN

View File

@@ -0,0 +1 @@
uid://b0xr7lumdnvci

View File

@@ -0,0 +1,9 @@
class_name Profile
extends SaveFile
@export var named_things: Dictionary[StringName, String] = {
"player_name": "Joel",
"villain_friend": "Josh",
"stupid_friend": "Kevin",
"favourite_candy": "Chocolate"
}

View File

@@ -0,0 +1 @@
uid://sx081r57538w

View File

@@ -0,0 +1,14 @@
class_name SaveFile
extends Resource
@export var save_name = "Save"
func _init(p_save_name = "Save") -> void:
save_name = p_save_name
func save() -> void: Save.save(self)
func load() -> void: Save.load(self)
func reset() -> void:
Save.file.erase_section(save_name)
Save.file.save(Save.save_path)

View File

@@ -0,0 +1 @@
uid://dru7lnuaadb87

51
shaders/pixelate.gdshader Normal file
View File

@@ -0,0 +1,51 @@
shader_type canvas_item;
/**
0 = Texture: For use with Sprite2Ds, TextureRects, and Meshes
1 = Screen: For use with ColorRect
*/
uniform int texture_mode : hint_range(0,1) = 1;
// This file relies on a quantize shader include file, listed further down.
// The paths must match your file's location.
#include "res://shaders/quantize.gdshaderinc"
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, repeat_disable, filter_nearest;
void vertex() {
float zoom = length(CANVAS_MATRIX[1].xyz);
v_quant_size = getQuantizeSize(zoom);
v_model_matrix = MODEL_MATRIX;
v_vertex = VERTEX;
if (texture_mode == 0) {
v_alt_matrix = inverse(MODEL_MATRIX);
v_texture_data.xy = 1. / TEXTURE_PIXEL_SIZE;
// Is texture flipped
v_texture_data.zw = max(-sign((UV - 0.5) * VERTEX), 0.);
} else {
v_alt_matrix = SCREEN_MATRIX * CANVAS_MATRIX;
if (snap_to_world) {
v_alt_matrix = inverse(v_alt_matrix);
}
vec2 local_origin = (MODEL_MATRIX * vec4(0.0, 0.0, 0, 1)).xy;
vec2 clip = (v_alt_matrix * vec4(local_origin, 0,1)).xy;
vec2 screen_origin_uv = clip * 0.5 + 0.5;
vec2 screen_pixel_size = 1. / vec2(textureSize(SCREEN_TEXTURE, 0));
vec2 q = screen_pixel_size * float(v_quant_size) * zoom;
v_texture_data.xy = screen_origin_uv;
v_texture_data.zw = q;
}
}
void fragment() {
if (texture_mode == 0) {
vec4 uvResult = getQuantizeTextureUV(UV);
COLOR = texture(TEXTURE, uvResult.xy);
} else {
vec4 uvResult = getQuantizeScreenUV(SCREEN_UV);
COLOR = texture(SCREEN_TEXTURE, uvResult.xy);
}
}

View File

@@ -0,0 +1 @@
uid://dgstw8crggyti

View File

@@ -0,0 +1,104 @@
group_uniforms Quantization;
/**Pixel resolution scale (0 is bypass)*/
uniform int quantize_size : hint_range(0,100) = 1;
/**Pixels are snapped based on world coordinates vs local coordinates.*/
uniform bool snap_to_world;
/**Auto-scales the quantize size when zoom is < 1, preventing subpixel artifacts*/
uniform bool limit_subpixels = true;
group_uniforms;
varying mat4 v_model_matrix;
varying mat4 v_alt_matrix;
varying vec2 v_vertex;
varying flat int v_quant_size;
varying flat vec4 v_texture_data;
const float EPSILON = 0.0001;
int getQuantizeSize(float in_zoom) {
int q_size = quantize_size;
if (limit_subpixels && in_zoom < 1.) {
q_size = int(round(float(quantize_size) * (1. / in_zoom)));
}
return q_size;
}
vec2 _snap(vec2 in_uv, float in_q_size) {
return (floor(in_uv / in_q_size) + 0.5) * in_q_size;
}
vec4 getQuantizeScreenUV(vec2 in_screen_uv) {
vec4 result;
if (v_quant_size == 0) {
result.xy = in_screen_uv;
result.zw = (v_model_matrix * vec4(v_vertex, 0, 1)).xy;
return result;
}
if (snap_to_world) {
vec2 uv = (v_model_matrix * vec4(v_vertex, 0, 1)).xy;
result.zw = uv + EPSILON;
uv = _snap(uv, float(v_quant_size));
uv = (v_alt_matrix * vec4(uv, 0,1)).xy;
uv = uv * 0.5 + 0.5;
result.xy = uv;
return result;
} else {
vec2 origin_uv = v_texture_data.xy;
vec2 quant_pixel_size = v_texture_data.zw;
vec2 uv = in_screen_uv - origin_uv;
uv = (floor(uv / quant_pixel_size) + 0.5) * quant_pixel_size;
uv = uv + origin_uv;
vec2 clipXY = uv * 2.0 - 1.;
vec2 world_pos = (v_alt_matrix * vec4(clipXY, 0.0, 1.0)).xy;
result.zw = world_pos + EPSILON;
result.xy = uv;
return result;
}
}
vec4 getQuantizeTextureUV(vec2 in_uv) {
vec4 result;
vec2 texture_size = v_texture_data.xy;
vec2 flip = v_texture_data.zw;
if (v_quant_size == 0) {
result.xy = in_uv;
result.zw = (v_model_matrix * vec4(in_uv * texture_size, 0, 1)).xy;
return result;
}
vec2 offset;
float q = float(v_quant_size);
vec2 uv = in_uv;
uv = mix(uv, 1.0 - uv, flip);
if (snap_to_world) {
vec2 inv_texture_size = 1. / texture_size;
offset = v_vertex * inv_texture_size;
offset = uv - offset;
uv -= offset;
uv *= texture_size;
uv = (v_model_matrix * vec4(uv, 0, 1)).xy;
result.zw = uv;
uv = _snap(uv, q);
uv = (v_alt_matrix * vec4(uv, 0, 1)).xy;
uv *= inv_texture_size;
uv = offset + uv;
} else {
uv *= texture_size;
result.zw = uv;
uv = _snap(uv, q);
uv /= texture_size;
}
uv = mix(uv, 1.0 - uv, flip);
result.xy = uv + EPSILON;
return result;
}

View File

@@ -0,0 +1 @@
uid://dp4ue4i3i5iaf