Initial commit

This commit is contained in:
Haze Weathers 2025-08-06 10:26:28 -06:00
commit 3b96451047
71 changed files with 2302 additions and 0 deletions

View file

@ -0,0 +1,174 @@
@tool
@icon("character.svg")
class_name WBCharacter
extends CharacterBody2D
## A character that can be controlled by events and behaviors and moves on a grid.
## Emitted when the character begins moving.
signal move_started()
## Emitted when the character reaches its target position.
signal move_finished()
enum Dir {LEFT, RIGHT, UP, DOWN}
const DIR_VECTORS: Dictionary[Dir, Vector2] = {
Dir.LEFT: Vector2.LEFT,
Dir.RIGHT: Vector2.RIGHT,
Dir.UP: Vector2.UP,
Dir.DOWN: Vector2.DOWN,
}
const DIR_ANIM_SUFFIXES: Dictionary[Dir, StringName] = {
Dir.LEFT: &"_left",
Dir.RIGHT: &"_right",
Dir.UP: &"_up",
Dir.DOWN: &"_down",
}
## Size of the grid the character is restricted to.
@export var tile_size: float = 16.0
## Speed the character walks at.
@export var walk_speed: float = 4.0
## Speed the character runs at.
@export var run_speed: float = 8.0
## Direction the character is facing.
@export var facing: Dir = Dir.DOWN
## Animation library for the character. [br]
## At a minimum, the [code]idle_*[/code] animations are required. [br]
## The following animations are used by default behavior: [br]
## [code]idle_[left,right,up,down][/code] [br]
## [code]walk_[left,right,up,down][/code] [br]
## [code]run_[left,right,up,down][/code] [br]
## [code]run_*[/code] will fallback to [code]walk_*[/code],
## which will fallback to [code]idle_*[/code]. [br]
## Addition custom animations may be provided to play on demand.
@export var animations: SpriteFrames:
set(value):
animations = value
sprite.sprite_frames = animations
## Texture drawing offset of the animated sprite.
@export var sprite_offset: Vector2 = Vector2.ZERO:
set(value):
sprite_offset = value
sprite.offset = sprite_offset
## True when the character is moving.
var moving: bool = false
## Whether the character is running.
var running: bool = false
## Tile position of the character on the grid.
var tile_position: Vector2i:
get():
return pos_to_tile(global_position)
var sprite: AnimatedSprite2D
var _next_pos: Vector2
var _playing_custom_animation: bool = false
func _init() -> void:
sprite = AnimatedSprite2D.new()
sprite.sprite_frames = animations
add_child(sprite)
func _ready() -> void:
global_position = closest_tile_center(global_position)
for child in get_children():
if child is CollisionShape2D or child is CollisionPolygon2D:
return
var col_shape := CollisionShape2D.new()
col_shape.shape = RectangleShape2D.new()
col_shape.shape.size = Vector2(tile_size - 2.0, tile_size - 2.0)
add_child(col_shape)
func _physics_process(delta: float) -> void:
if moving:
var move_delta := (run_speed if running else walk_speed) * tile_size * delta
global_position = global_position.move_toward(_next_pos, move_delta)
if global_position == _next_pos:
moving = false
move_finished.emit()
func _process(delta: float) -> void:
if moving:
_playing_custom_animation = false
var anims: Array[StringName] = [
&"walk" + DIR_ANIM_SUFFIXES[facing],
&"idle" + DIR_ANIM_SUFFIXES[facing]
]
if running:
anims.push_front(&"run" + DIR_ANIM_SUFFIXES[facing])
_try_animations(anims)
elif not _playing_custom_animation:
_try_animations([&"idle" + DIR_ANIM_SUFFIXES[facing]])
## Makes the character move one tile in the given direction. [br]
## If [param ignore_collision] is true, the character will not perform collision checks.
func start_move(dir: Dir, ignore_collision: bool = false) -> bool:
if moving:
return false
facing = dir
_next_pos = global_position + DIR_VECTORS[dir] * tile_size
var col := move_and_collide(_next_pos - global_position, true)
if col and not ignore_collision:
return false
moving = true
move_started.emit()
return true
## Plays a given custom animation from the animation set. [br]
## If [param reset_after] is [constant true], the animation will return to
## the default idle animation after it finishes.
func play_custom_animation(anim: StringName, reset_after: bool = false) -> void:
_try_animations([anim])
_playing_custom_animation = true
if reset_after and not animations.get_animation_loop(anim):
await sprite.animation_finished
_playing_custom_animation = false
## Stops playing custom animation if one is currently playing.
func end_custom_animation() -> void:
_playing_custom_animation = false
## Returns the closest tile center position to a given position in global coordinates.
func closest_tile_center(pos: Vector2) -> Vector2:
var tile := pos - Vector2(tile_size, tile_size) * 0.5
tile = tile.snappedf(tile_size)
tile += Vector2(tile_size, tile_size) * 0.5
return tile
## Returns the tile coordinates of a given position in global coordinates.
func pos_to_tile(pos: Vector2) -> Vector2i:
return Vector2i((global_position / Vector2(tile_size, tile_size)).floor())
## Returns the center position of a given tile in global coordinates.
func tile_center_pos(tile: Vector2i) -> Vector2:
return (Vector2(tile) * Vector2(tile_size, tile_size)) + (Vector2(tile_size, tile_size) * 0.5)
func _try_animations(anims: Array[StringName]) -> void:
for anim in anims:
if animations.has_animation(anim):
sprite.play(anim)
return

View file

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

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="character.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
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">
<linearGradient
id="a"
x2="0"
y1="2"
y2="14"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4)">
<stop
offset="0"
stop-color="#ff8dbc"
id="stop1" />
<stop
offset=".4"
stop-color="#7260ff"
id="stop2" />
<stop
offset=".6"
stop-color="#7260ff"
id="stop3" />
<stop
offset="1"
stop-color="#74c9ff"
id="stop4" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="14.854978"
inkscape:cx="0.67317502"
inkscape:cy="10.434213"
inkscape:window-width="1346"
inkscape:window-height="727"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
fill="#8da5f3"
d="m 6.4922281,1 a 1,1 0 0 0 -1,1 v 3 a 1,1 0 0 0 1,1 h 1 v 0.99 a 1,1 0 0 0 -0.316,0.062 l -2.051,0.684 -0.684,-2.051 a 1.0000706,1.0000706 0 0 0 -1.898,0.631 l 1,3 a 1,1 0 0 0 1.265,0.633 l 1.684,-0.56 v 0.61 c 0,0.041 0.019,0.076 0.024,0.116 l -4.579,3.052 a 1.0001245,1.0001245 0 1 0 1.11,1.664 l 5.056,-3.37 1.495,2.986 a 1,1 0 0 0 1.2100009,0.502 l 3,-1 a 1,1 0 1 0 -0.632,-1.897 l -2.178,0.725 -0.975001,-1.951 A 0.981,0.981 0 0 0 10.492229,9.999 V 9 h 1.382999 l 0.722,1.448 a 1.0006409,1.0006409 0 1 0 1.790001,-0.895 l -1,-2 A 1,1 0 0 0 12.492229,7 H 9.4922281 V 6 h 1.0000009 a 1,1 0 0 0 1,-1 V 2 a 1,1 0 0 0 -1,-1 z m 0,2 h 1 v 2 h -1 z"
id="path1"
style="fill:#20c997" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ufpt8ejikj1j"
path="res://.godot/imported/character.svg-492c7305b4930c6092d7995fed4891a3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/walkabout/characters/character.svg"
dest_files=["res://.godot/imported/character.svg-492c7305b4930c6092d7995fed4891a3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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,44 @@
@tool
@icon("character_behavior.svg")
class_name WBCharacterBehavior
extends Node
## Controls/puppets a parent character.
## Whether the behavior is currently active.
@export var active: bool:
set(value):
var last_value := active
active = value
if active:
if not is_node_ready():
await ready
for child in get_parent().get_children():
if child != self and child is WBCharacterBehavior:
child.active = false
if active:
_activate()
else:
_deactivate()
## The character being controlled.
var character: WBCharacter:
get():
return get_parent() as WBCharacter
## Enables the behavior.
func enable() -> void:
active = true
## Disables the behavior.
func disable() -> void:
active = false
func _activate() -> void:
pass
func _deactivate() -> void:
pass

View file

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

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="character_behavior.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
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">
<linearGradient
id="a"
x2="0"
y1="2"
y2="14"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4)">
<stop
offset="0"
stop-color="#ff8dbc"
id="stop1" />
<stop
offset=".4"
stop-color="#7260ff"
id="stop2" />
<stop
offset=".6"
stop-color="#7260ff"
id="stop3" />
<stop
offset="1"
stop-color="#74c9ff"
id="stop4" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="14.854978"
inkscape:cx="0.67317502"
inkscape:cy="10.434213"
inkscape:window-width="1346"
inkscape:window-height="727"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
fill="none"
stroke="#8da5f3"
stroke-linejoin="round"
stroke-width="2.28571"
d="M 14.857145,14.857148 H 1.1428572 V 1.1428572 H 14.857145 Z"
id="path1"
style="stroke:#20c997;stroke-width:2;stroke-dasharray:none" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://lib4e4qnml8a"
path="res://.godot/imported/character_behavior.svg-d7dbdf997daf0ba87cdd58e5db5e61e1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/walkabout/characters/character_behavior.svg"
dest_files=["res://.godot/imported/character_behavior.svg-d7dbdf997daf0ba87cdd58e5db5e61e1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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,81 @@
@tool
@icon("path_behavior.svg")
class_name WBPathBehavior
extends WBCharacterBehavior
## A behavior that makes a character walk along a specified path,
## looping when they reach the end.
## List of points to walk to.
@export var points: Array[Vector2i]
## Whether the character should be running.
@export var run: bool = false
var _current_point: int
func _init() -> void:
if Engine.is_editor_hint():
add_child(DebugView.new(self))
func _activate() -> void:
_do_move()
func _do_move() -> void:
if not active:
return
if character.tile_position == points[_current_point]:
_current_point = posmod(_current_point + 1, points.size())
var point := points[_current_point]
if character.tile_position.x < point.x:
character.start_move(WBCharacter.Dir.RIGHT)
elif character.tile_position.x > point.x:
character.start_move(WBCharacter.Dir.LEFT)
elif character.tile_position.y < point.y:
character.start_move(WBCharacter.Dir.DOWN)
elif character.tile_position.y > point.y:
character.start_move(WBCharacter.Dir.UP)
if character.moving:
await character.move_finished
_do_move()
class DebugView extends Node2D:
var behavior: WBPathBehavior
func _init(p_behavior: WBPathBehavior) -> void:
behavior = p_behavior
func _process(delta: float) -> void:
if Engine.get_frames_drawn() % 60 == 0:
queue_redraw()
func _draw() -> void:
if not behavior.character:
return
var color := Color.MEDIUM_PURPLE
var width := -1.0
var selection := EditorInterface.get_selection()
if behavior in selection.get_selected_nodes():
color = Color.PURPLE
width = 2.0
var last_point := to_local(behavior.character.closest_tile_center(behavior.character.global_position))
for point in behavior.points + [behavior.points[0]]:
var current_point := to_local(behavior.character.tile_center_pos(point))
draw_line(last_point, Vector2(current_point.x, last_point.y), color, width)
draw_line(Vector2(current_point.x, last_point.y), current_point, color, width)
var arrow_dir := current_point.direction_to(Vector2(current_point.x, last_point.y))
if arrow_dir.is_zero_approx():
arrow_dir = current_point.direction_to(last_point)
draw_line(current_point, current_point + arrow_dir.rotated(deg_to_rad(45.0)) * 8.0, color, width)
draw_line(current_point, current_point + arrow_dir.rotated(deg_to_rad(-45.0)) * 8.0, color, width)
last_point = current_point

View file

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

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="path_behavior.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
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">
<linearGradient
id="a"
x2="0"
y1="2"
y2="14"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4)">
<stop
offset="0"
stop-color="#ff8dbc"
id="stop1" />
<stop
offset=".4"
stop-color="#7260ff"
id="stop2" />
<stop
offset=".6"
stop-color="#7260ff"
id="stop3" />
<stop
offset="1"
stop-color="#74c9ff"
id="stop4" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="14.854978"
inkscape:cx="0.67317502"
inkscape:cy="10.434213"
inkscape:window-width="1346"
inkscape:window-height="727"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
fill="none"
stroke="#8da5f3"
stroke-linejoin="round"
stroke-width="2.28571"
d="M 14.857145,14.857148 H 1.1428572 V 1.1428572 H 14.857145 Z"
id="path1"
style="stroke:#20c997;stroke-width:2;stroke-dasharray:none" />
<path
style="fill:none;fill-opacity:1;stroke:#20c997;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
d="M 4.2410027,11.511293 4.1736852,4.3083202 8.5493228,4.7122252 8.2127353,7.7415128 11.915198,8.2127353 11.242023,12.11715 Z"
id="path8" />
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3los038dn424"
path="res://.godot/imported/path_behavior.svg-92b1066b81b0693c0689fa68366f00d0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/walkabout/characters/path_behavior.svg"
dest_files=["res://.godot/imported/path_behavior.svg-92b1066b81b0693c0689fa68366f00d0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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,98 @@
@tool
@icon("player_character.svg")
class_name WBPlayerCharacter
extends WBCharacter
## A character controlled directly by the player.
## Whether the character will react to player input.
@export var controllable: bool = true
## Time to wait after turning before the character will start moving.
@export var turn_walk_time: float = 0.25
## Collision mask for interactive objects.
## If a collision is detected, [method interact] will be called on the target object if it exists.
@export_flags_2d_physics var interact_mask: int:
set(value):
interact_mask = value
_interact_raycast.collision_mask = interact_mask
@export_group("Input Actions", "input_")
@export var input_left: StringName = &"ui_left"
@export var input_right: StringName = &"ui_right"
@export var input_up: StringName = &"ui_up"
@export var input_down: StringName = &"ui_down"
@export var input_interact: StringName = &"ui_accept"
@export var input_run: StringName = &"ui_cancel"
var _interact_raycast: RayCast2D
var _turn_cooldown: float = 0.0
func _init() -> void:
super._init()
move_finished.connect(_check_move_input)
_interact_raycast = RayCast2D.new()
_interact_raycast.enabled = false
_interact_raycast.collide_with_areas = true
_interact_raycast.hit_from_inside = true
_interact_raycast.add_exception(self)
add_child(_interact_raycast)
func _physics_process(delta: float) -> void:
if Engine.is_editor_hint():
return
_turn_cooldown -= delta
running = Input.is_action_pressed(input_run)
if not moving:
_check_move_input(true)
super._physics_process(delta)
## Enables player control of the character.
func enable_control() -> void:
controllable = true
## Disables player control of the character.
func disable_control() -> void:
controllable = false
func _check_move_input(check_turn_cooldown: bool = false) -> void:
if not controllable:
return
if Input.is_action_pressed(input_left):
_try_move(Dir.LEFT, check_turn_cooldown)
elif Input.is_action_pressed(input_right):
_try_move(Dir.RIGHT, check_turn_cooldown)
elif Input.is_action_pressed(input_up):
_try_move(Dir.UP, check_turn_cooldown)
elif Input.is_action_pressed(input_down):
_try_move(Dir.DOWN, check_turn_cooldown)
elif Input.is_action_just_pressed(input_interact):
_try_interact()
func _try_move(dir: Dir, check_turn_cooldown: bool) -> void:
if facing != dir:
_turn_cooldown = turn_walk_time
if running:
_turn_cooldown *= 0.5
facing = dir
if check_turn_cooldown and _turn_cooldown >= 0.0:
return
start_move(dir)
func _try_interact() -> void:
_interact_raycast.target_position = DIR_VECTORS[facing] * tile_size
_interact_raycast.force_raycast_update()
if _interact_raycast.is_colliding():
var interactive := _interact_raycast.get_collider()
if interactive.has_method(&"interact"):
interactive.interact()

View file

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

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="player_character.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
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">
<linearGradient
id="a"
x2="0"
y1="2"
y2="14"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4)">
<stop
offset="0"
stop-color="#ff8dbc"
id="stop1" />
<stop
offset=".4"
stop-color="#7260ff"
id="stop2" />
<stop
offset=".6"
stop-color="#7260ff"
id="stop3" />
<stop
offset="1"
stop-color="#74c9ff"
id="stop4" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="25.753231"
inkscape:cx="4.5042892"
inkscape:cy="6.6787737"
inkscape:window-width="1346"
inkscape:window-height="727"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
fill="#8da5f3"
d="m 7.4922281,6.99 c -0.1079911,0.00346 -0.2147078,0.024398 -0.316,0.062 l -2.051,0.684 -0.684,-2.051 C 4.0205616,4.4196672 2.1225616,5.0506672 2.5432281,6.316 l 1,3 c 0.1744187,0.5242077 0.7408721,0.807658 1.265,0.633 l 1.684,-0.56 v 0.61 c 0,0.041 0.019,0.076 0.024,0.116 l -4.579,3.052 c -1.10956307,0.739954 4.369e-4,2.403954 1.11,1.664 l 5.056,-3.37 1.495,2.986 c 0.2223325,0.444998 0.7379489,0.658915 1.2100009,0.502 l 3,-1 c 1.302584,-0.402102 0.651473,-2.356463 -0.632,-1.897 l -2.178,0.725 -0.975001,-1.951 c 0.288394,-0.176434 0.465617,-0.488934 0.469001,-0.827 V 9 h 1.382999 l 0.722,1.448 c 0.596513,1.193792 2.386514,0.298792 1.790001,-0.895 l -1,-2 C 13.217804,7.2139484 12.871255,6.9998234 12.492229,7 H 9.4922281 Z"
id="path1"
style="fill:#20c997"
sodipodi:nodetypes="ccccccccsccccccccccccccccccc" />
<path
style="fill:#20c997;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
d="M 8.7367679,7.0282444 7.843676,4.89259 5.1644006,4.0383283 7.843676,2.951086 9.1250687,0.46596095 9.78518,3.1064064 12.969247,3.8830079 9.6686898,4.8537599 9.7463498,3.1452364 7.882506,3.0287462 l 0.03883,1.7861836 1.7861837,0.07766 z"
id="path8" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdwrfpa2xn5cf"
path="res://.godot/imported/player_character.svg-b26ca4ad6a1901b6ebacace37c911113.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/walkabout/characters/player_character.svg"
dest_files=["res://.godot/imported/player_character.svg-b26ca4ad6a1901b6ebacace37c911113.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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,78 @@
@tool
@icon("wander_behavior.svg")
class_name WBWanderBehavior
extends WBCharacterBehavior
## A behavior that makes a character randomly wander around within a specified area.
## Rectangle enclosing the tiles the character is allowed to move to.
@export var territory: Rect2i
## Minimum distance to travel in one "burst".
@export var min_distance: int
## Maximum distance to travel in one "burst".
@export var max_distance: int
## Minimum time to idle after finishing a "burst".
@export_custom(0, "suffix:s") var min_idle: float
## Maximum time to idle after finishing a "burst".
@export_custom(0, "suffix:s") var max_idle: float
## Whether the character should run.
@export var run: bool = false
func _init() -> void:
if Engine.is_editor_hint():
add_child(DebugView.new(self))
func _activate() -> void:
if character:
_do_move()
func _do_move() -> void:
if not active:
return
var remaining_distance: int = randi_range(min_distance, max_distance)
var dir: WBCharacter.Dir = WBCharacter.Dir.values().pick_random()
while remaining_distance > 0:
while not territory.has_point(character.tile_position + Vector2i(WBCharacter.DIR_VECTORS[dir])):
dir = WBCharacter.Dir.values().pick_random()
character.running = run
character.start_move(dir)
if character.moving:
await character.move_finished
remaining_distance -= 1
await create_tween().tween_interval(randf_range(min_idle, max_idle)).finished
_do_move()
class DebugView extends Node2D:
var behavior: WBWanderBehavior
func _init(p_behavior: WBWanderBehavior) -> void:
behavior = p_behavior
func _process(delta: float) -> void:
if Engine.get_frames_drawn() % 60 == 0:
queue_redraw()
func _draw() -> void:
if not behavior.character:
return
var position := to_local(Vector2(behavior.territory.position) * behavior.character.tile_size)
var size := Vector2(behavior.territory.size) * behavior.character.tile_size
var color := Color.MEDIUM_PURPLE
var width := -1.0
var selection := EditorInterface.get_selection()
if behavior in selection.get_selected_nodes():
color = Color.PURPLE
width = 2.0
draw_rect(
Rect2(position, size),
color, false, width
)

View file

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

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="wander_behavior.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
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">
<linearGradient
id="a"
x2="0"
y1="2"
y2="14"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4)">
<stop
offset="0"
stop-color="#ff8dbc"
id="stop1" />
<stop
offset=".4"
stop-color="#7260ff"
id="stop2" />
<stop
offset=".6"
stop-color="#7260ff"
id="stop3" />
<stop
offset="1"
stop-color="#74c9ff"
id="stop4" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="14.854978"
inkscape:cx="0.67317502"
inkscape:cy="10.434213"
inkscape:window-width="1346"
inkscape:window-height="727"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
fill="none"
stroke="#8da5f3"
stroke-linejoin="round"
stroke-width="2.28571"
d="M 14.857145,14.857148 H 1.1428572 V 1.1428572 H 14.857145 Z"
id="path1"
style="stroke:#20c997;stroke-width:2;stroke-dasharray:none" />
<path
style="fill:#20c997;fill-opacity:1;stroke:#20c997;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
d="M 3.9276284,7.9330316 5.599787,7.0386211 5.5608995,8.8663295 Z"
id="path5"
sodipodi:nodetypes="cccc" />
<path
style="fill:#20c997;fill-opacity:1;stroke:#20c997;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
d="M 11.501205,7.9790821 9.8290464,8.8734926 9.8679344,7.0457842 Z"
id="path6"
sodipodi:nodetypes="cccc" />
<path
style="fill:#20c997;fill-opacity:1;stroke:#20c997;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
d="M 7.7568856,4.1887121 8.6512961,5.8608707 6.8235877,5.8219827 Z"
id="path7"
sodipodi:nodetypes="cccc" />
<path
style="fill:#20c997;fill-opacity:1;stroke:#20c997;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
d="M 7.5941729,11.762289 6.6997624,10.09013 8.5274708,10.129018 Z"
id="path8"
sodipodi:nodetypes="cccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dcb3i7xdowdte"
path="res://.godot/imported/wander_behavior.svg-cec028e106c64cd15d62b2fb10cf549b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/walkabout/characters/wander_behavior.svg"
dest_files=["res://.godot/imported/wander_behavior.svg-cec028e106c64cd15d62b2fb10cf549b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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