const FWD = { MetroidvaniaSystem.R: Vector2i.RIGHT, MetroidvaniaSystem.D: Vector2i.DOWN, MetroidvaniaSystem.L: Vector2i.LEFT, MetroidvaniaSystem.U: Vector2i.UP } class CellData: enum { DATA_EXITS, DATA_COLORS, DATA_SYMBOL, DATA_MAP, OVERRIDE_COORDS, OVERRIDE_CUSTOM } var color: Color = Color.TRANSPARENT var borders: Array[int] = [-1, -1, -1, -1] var border_colors: Array[Color] = [Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT] var symbol := -1 var assigned_scene: String var override_map: String var loading func _init(line: String) -> void: if line.is_empty(): return loading = [line, -1] var chunk := load_next_chunk() for i in 4: borders[i] = chunk.get_slice(",", i).to_int() chunk = load_next_chunk() if not chunk.is_empty(): var color_slice := chunk.get_slice(",", 0) if not color_slice.is_empty(): color = Color(color_slice) for i in 4: color_slice = chunk.get_slice(",", i + 1) if not color_slice.is_empty(): border_colors[i] = Color(color_slice) chunk = load_next_chunk() if not chunk.is_empty(): symbol = chunk.to_int() assigned_scene = load_next_chunk() loading = null func get_string() -> String: var data: PackedStringArray data.append("%s,%s,%s,%s" % borders) var colors: Array[Color] colors.assign([color] + Array(border_colors)) if colors.any(func(col: Color): return col.a > 0): data.append("%s,%s,%s,%s,%s" % colors.map(func(col: Color): return col.to_html(false) if col.a > 0 else "")) else: data.append("") if symbol > -1: data.append(str(symbol)) else: data.append("") data.append(assigned_scene.trim_prefix(MetSys.settings.map_root_folder + "/")) return "|".join(data) func load_next_chunk() -> String: loading[1] += 1 return loading[0].get_slice("|", loading[1]) func get_color() -> Color: var c: Color var override := get_override() if override and override.color.a > 0: c = override.color else: c = color if c.a > 0: return c return MetSys.settings.theme.default_center_color func get_border(idx: int) -> int: var override := get_override() if override and override.borders[idx] != -2: return override.borders[idx] return borders[idx] func get_border_color(idx: int) -> Color: var c: Color var override := get_override() if override and override.border_colors[idx].a > 0: c = override.border_colors[idx] else: c = border_colors[idx] if c.a > 0: return c return MetSys.settings.theme.default_border_color func get_symbol() -> int: var override := get_override() if override and override.symbol != -2: return override.symbol return symbol func get_assigned_scene() -> String: var override := get_override() if override and override.assigned_scene != "/": return override.assigned_scene if not override_map.is_empty(): return override_map return assigned_scene func get_override() -> CellOverride: if not MetSys.save_data: return null return MetSys.save_data.cell_overrides.get(self) func get_coords() -> Vector3i: return MetSys.map_data.cells.find_key(self) ## A runtime override for the cell's properties that allows to modify it's default appearance designed in the editor. ## ## CellOverride can be obtained from [method MetroidvaniaSystem.get_cell_override] or using the MapBuilder. It comes with a set of methods that allows customizing the cells. [signal MetroidvaniaSystem.map_updated] signal is automatically emitted after modifications, unless the cell comes from MapBuilder. class CellOverride extends CellData: var original_room: CellData var custom_cell_coords := Vector3i.MAX var commit_queued: bool func _init(from: CellData) -> void: original_room = from borders = [-2, -2, -2, -2] symbol = -2 assigned_scene = "/" static func load_from_line(line: String) -> CellOverride: var cell: CellData var coords_string := line.get_slice("|", CellData.OVERRIDE_COORDS) var coords := Vector3i(coords_string.get_slice(",", 0).to_int(), coords_string.get_slice(",", 1).to_int(), coords_string.get_slice(",", 2).to_int()) var is_custom := line.get_slice("|", CellData.OVERRIDE_CUSTOM) == "true" if is_custom: cell = MetSys.map_data.create_cell_at(coords) else: cell = MetSys.map_data.get_cell_at(coords) var override := CellOverride.new(cell) if is_custom: override.custom_cell_coords = coords var fake_cell := CellData.new(line) override.borders = fake_cell.borders override.border_colors = fake_cell.border_colors override.color = fake_cell.color override.symbol = fake_cell.symbol override.set_assigned_scene(fake_cell.assigned_scene) return override ## Sets a border of the cell. [param idx] is the direction index of the cell. Use [constant MetroidvaniaSystem.R], [constant MetroidvaniaSystem.D], [constant MetroidvaniaSystem.L], [constant MetroidvaniaSystem.U] constants here. The [param value] must be within the [member MapTheme.borders] array bounds + 2 (0 and 1 are the default wall and passage). Use the default to reset to the border assigned in the editor. Value of [code]-1[/code] will remove the border, but it's not recommended. func set_border(idx: int, value := -2): assert(idx >= 0 and idx < 4) borders[idx] = value _queue_commit() ## Sets the color of a cell's border. [param idx] is the direction index of the cell (see [method set_border]). If the default [param value] is used (or any color with [code]0[/code] alpha), the border will be reset to the default color. func set_border_color(idx: int, value := Color.TRANSPARENT): assert(idx >= 0 and idx < 4) border_colors[idx] = value _queue_commit() ## Sets the color of the cell's center texture. func set_color(value := Color.TRANSPARENT): color = value _queue_commit() ## Sets the cell's assigned symbol. Value of [code]-1[/code] will remove the symbol. The markers assigned from [method MetroidvaniaSystem.set_custom_marker] still have a priority. func set_symbol(value := -2): assert(value >= -2 and value < MetSys.settings.theme.symbols.size()) symbol = value _queue_commit() ## Changes the scene assigned to the cell. Unlike other methods, this has effect on the whole room that contains this cell. Using the default value will reset it to the scene assigned in the editor. func set_assigned_scene(value := "/"): if value == "/": _cleanup_assigned_scene() else: if custom_cell_coords != Vector3i.MAX: if not value in MetSys.map_data.assigned_scenes: MetSys.map_data.assigned_scenes[value] = [] MetSys.map_data.assigned_scenes[value].append(custom_cell_coords) else: MetSys.map_data.scene_overrides[value] = original_room.assigned_scene for coords in MetSys.map_data.get_whole_room(original_room.get_coords()): var cell: CellData = MetSys.map_data.cells[coords] if not cell.override_map.is_empty(): push_warning("Assigned map already overriden at: %s" % coords) cell.override_map = value assigned_scene = value MetSys.room_assign_updated.emit() _queue_commit() ## Applies the values from this CellOverride to all cells that belong to the specified [param group_id]. You need to apply your customization [i]before[/i] calling this method. func apply_to_group(group_id: int): assert(group_id in MetSys.map_data.cell_groups) for coords in MetSys.map_data.cell_groups[group_id]: var override: CellOverride = MetSys.get_cell_override(coords) if override == self: continue override.borders = borders.duplicate() override.color = color override.border_colors = border_colors.duplicate() override.symbol = symbol _queue_commit() ## Destroys the cell. This method can only be used on the overrides from the MapBuilder. func destroy() -> void: if custom_cell_coords == Vector3i.MAX: push_error("Only custom cell can be destroyed.") return var cell_coords := custom_cell_coords custom_cell_coords = Vector3i.MAX MetSys.remove_cell_override(cell_coords) MetSys.map_data.erase_cell(cell_coords) MetSys.map_data.cell_overrides.erase(cell_coords) MetSys.map_data.custom_cells.erase(self) if assigned_scene != "/": MetSys.map_data.assigned_scenes[assigned_scene].erase(cell_coords) if MetSys.save_data: MetSys.save_data.discovered_cells.erase(cell_coords) func _cleanup_assigned_scene() -> void: if assigned_scene == "/": return MetSys.map_data.scene_overrides.erase(assigned_scene) for coords in MetSys.map_data.get_whole_room(original_room.get_coords()): MetSys.map_data.cells[coords].override_map = "" func _get_override_string(coords: Vector3i) -> String: return str(get_string(), "|", coords.x, ",", coords.y, ",", coords.z, "|", custom_cell_coords != Vector3i.MAX) func _queue_commit(): if commit_queued or custom_cell_coords != Vector3i.MAX: return commit_queued = true _commit.call_deferred() func _commit() -> void: commit_queued = false MetSys.map_updated.emit() var cells: Dictionary#[Vector3i, CellData] var custom_cells: Dictionary#[Vector3i, CellData] var assigned_scenes: Dictionary#[String, Array[Vector3i]] var cell_groups: Dictionary#[int, Array[Vector3i]] var custom_elements: Dictionary#[Vector3i, Struct] var cell_overrides: Dictionary#[Vector3i, CellOverride] var scene_overrides: Dictionary#[String, String] func load_data(): var file := FileAccess.open(MetSys.settings.map_setting_folder.path_join("MapData.txt"), FileAccess.READ) if not file: push_warning("Map data file does not exist.") return var data := file.get_as_text().split("\n") var i: int var current_section := 0 # groups, custom_elements, cells while i < data.size(): var line := data[i] if line.begins_with("["): current_section = 2 line = line.trim_prefix("[").trim_suffix("]") var coords: Vector3i coords.x = line.get_slice(",", 0).to_int() coords.y = line.get_slice(",", 1).to_int() coords.z = line.get_slice(",", 2).to_int() i += 1 line = data[i] var cell_data := CellData.new(line) if not cell_data.assigned_scene.is_empty(): assigned_scenes[cell_data.assigned_scene] = [coords] cells[coords] = cell_data elif current_section == 1 or current_section == 0 and line.contains("/"): current_section = 1 var element_data := line.split("/") var element: Dictionary element.name = element_data[1] element.size = Vector2i(element_data[2].get_slice("x", 0).to_int(), element_data[2].get_slice("x", 1).to_int()) if element_data.size() == 4: element.data = element_data[3] else: element.data = "" var coords: Vector3i coords.x = element_data[0].get_slice(",", 0).to_int() coords.y = element_data[0].get_slice(",", 1).to_int() coords.z = element_data[0].get_slice(",", 2).to_int() custom_elements[coords] = element elif current_section == 0: var group_data := line.split(":") var group_id := group_data[0].to_int() var rooms_in_group: Array for j in range(1, group_data.size()): var coords: Vector3i coords.x = group_data[j].get_slice(",", 0).to_int() coords.y = group_data[j].get_slice(",", 1).to_int() coords.z = group_data[j].get_slice(",", 2).to_int() rooms_in_group.append(coords) cell_groups[group_id] = rooms_in_group i += 1 for map in assigned_scenes.keys(): var assigned_cells: Array[Vector3i] assigned_cells.assign(assigned_scenes[map]) assigned_scenes[map] = get_whole_room(assigned_cells[0]) func save_data(): var file := FileAccess.open(MetSys.settings.map_setting_folder.path_join("MapData.txt"), FileAccess.WRITE) if not file: push_error("Could not open file '%s' for writing." % MetSys.settings.map_setting_folder.path_join("MapData.txt")) return for group in cell_groups: if cell_groups[group].is_empty(): continue var line: PackedStringArray line.append(str(group)) for coords in cell_groups[group]: line.append("%s,%s,%s" % [coords.x, coords.y, coords.z]) file.store_line(":".join(line)) for coords in custom_elements: var line: PackedStringArray line.append("%s,%s,%s" % [coords.x, coords.y, coords.z]) var element: Dictionary = custom_elements[coords] line.append(element.name) line.append("%sx%s" % [element.size.x, element.size.y]) if not element.data.is_empty(): line.append(element.data) file.store_line("/".join(line)) for coords in cells: file.store_line("[%s,%s,%s]" % [coords.x, coords.y, coords.z]) var cell_data := get_cell_at(coords) file.store_line(cell_data.get_string()) func get_cell_at(coords: Vector3i) -> CellData: return cells.get(coords) func create_cell_at(coords: Vector3i) -> CellData: cells[coords] = CellData.new("") return cells[coords] func create_custom_cell(coords: Vector3i) -> CellOverride: assert(not coords in cells, "A cell already exists at this position") var cell := create_cell_at(coords) custom_cells[coords] = cell var override: CellOverride = MetSys.save_data.add_cell_override(cell) override.custom_cell_coords = coords return override func get_whole_room(at: Vector3i) -> Array[Vector3i]: var room: Array[Vector3i] var to_check: Array[Vector2i] = [Vector2i(at.x, at.y)] var checked: Array[Vector2i] while not to_check.is_empty(): var p: Vector2i = to_check.pop_back() checked.append(p) var coords := Vector3i(p.x, p.y, at.z) if coords in cells: room.append(coords) for i in 4: if cells[coords].borders[i] == -1: var p2: Vector2i = p + FWD[i] if not p2 in to_check and not p2 in checked: to_check.append(p2) return room func get_cells_assigned_to(map: String) -> Array[Vector3i]: if map in scene_overrides: map = scene_overrides[map] var ret: Array[Vector3i] ret.assign(assigned_scenes.get(map, [])) return ret func get_assigned_scene_at(coords: Vector3i) -> String: var cell := get_cell_at(coords) if cell: return cell.get_assigned_scene() else: return "" func erase_cell(coords: Vector3i): var assigned_scene: String = cells[coords].assigned_scene assigned_scenes.erase(assigned_scene) cells.erase(coords) for group in cell_groups.values(): group.erase(coords)