Extensive work on VFX for the guild, assets for the world, and portrait variance. Work on quests. Extra work on User Flow completion and file saving.

This commit is contained in:
2025-09-04 07:46:55 -04:00
parent 149ee993dc
commit 48e335f56a
134 changed files with 2232 additions and 288 deletions

View File

@@ -1,5 +1,11 @@
class_name Adventurer extends Node
class Appearance extends Resource:
var hair_color : String
var hair_type : int
var skin_color : String
var eye_color : String
var eye_type : int
enum Gender{
MASC,
@@ -17,12 +23,11 @@ var max_energy : int = 1
var level : int = 1
var exp : int = 0
var job : JobData
var appearance : Appearance
var stats : StatBlock
var gold : int = 0
var quest : Quest
var weapon : Weapon
var armor : Armor
var accessory : Accessory
var equipment : Array = [null, null, null]
var inventory : Dictionary[Vector2, Item] = {}
var inventory_size : Vector2i = Vector2i(3,2)
var inventory_count : int = 0
@@ -45,7 +50,7 @@ func generate() -> void:
stats.CHA = randi_range(job.min_CHA, job.max_CHA)
stats.FAI = randi_range(job.min_FAI, job.max_FAI)
stats.LUK = randi_range(job.min_LUK, job.max_LUK)
generate_appearance()
max_life = stats.STR * 10 + stats.CHA * 10
max_energy = stats.INT * 10 + stats.FAI * 10
life = max_life
@@ -64,7 +69,6 @@ func gain_level() -> void:
level += 1
#TODO: Make stats improve based on job
Game.notice("%s has reached level %d!" % [full_name(), level])
#TODO: Make Sideview display level up
changed.emit()
levelled.emit()
@@ -115,6 +119,50 @@ func pickup_item(item : Item) -> void:
inventory_count+=1
changed.emit()
func equip_item(from : Vector2, slot : Item.Slots) -> void:
if !inventory.has(from):
printerr("Cannot equip item from %s, it is empty!" % [from])
var itm = inventory[from]
if !itm.can_equip_slot(slot):
printerr("Cannot equip item from %s to %s, it is the wrong type!" % [from, Item.slot_name(slot)])
if !job.can_equip(itm):
printerr("Cannot equip item from %s to %s, %s cannot equip it!" % [from, Item.slot_name(slot), job.name])
if equipment[slot] != null:
inventory[from] = equipment[slot]
remove_stats_from_item(itm)
else:
inventory.erase(from)
equipment[slot] = itm
apply_stats_from_item(itm)
#Apply Stats from
func remove_stats_from_item(itm : Item) -> void:
stats.STR -= stats.STR
stats.DEX-= stats.DEX
stats.INT-= stats.INT
stats.CHA-= stats.CHA
stats.FAI-= stats.FAI
stats.LUK-= stats.LUK
stats.PATK-= stats.PATK
stats.MATK-= stats.MATK
stats.PDEF-= stats.PDEF
stats.MDEF-= stats.MDEF
func apply_stats_from_item(itm : Item) -> void:
stats.STR += stats.STR
stats.DEX+= stats.DEX
stats.INT+= stats.INT
stats.CHA+= stats.CHA
stats.FAI+= stats.FAI
stats.LUK+= stats.LUK
stats.PATK+= stats.PATK
stats.MATK+= stats.MATK
stats.PDEF+= stats.PDEF
stats.MDEF+= stats.MDEF
func move_item(from : Vector2, to: Vector2) -> void:
if !inventory.has(from):
printerr("Cannot move item from %s to %s, %s is empty!" % [from, to, from])
@@ -126,3 +174,14 @@ func move_item(from : Vector2, to: Vector2) -> void:
inventory.erase(from)
inventory[to] = itm
changed.emit()
func generate_appearance(features=null) -> void:
if features == null:
appearance = Appearance.new()
#TODO: Handle different types of hair and eyes
appearance.hair_color = AdventurerPortrait.random_color(ColorVariant.Types.HAIR)
#appearance.hair_type = randi_range(0,len(job.portrait.hair_types) - 1)
appearance.eye_color = AdventurerPortrait.random_color(ColorVariant.Types.EYES)
#appearance.eye_type = randi_range(0,len(job.portrait.eye_types) - 1)
appearance.skin_color = AdventurerPortrait.random_color(ColorVariant.Types.SKIN)
changed.emit()

View File

@@ -1 +1,41 @@
extends Control
class_name AdventurerPortrait extends Control
var cv_lists : Array
func _ready() -> void:
cv_lists = []
cv_lists.resize(ColorVariant.Types.size())
for i in range(len(cv_lists)):
cv_lists[i] = []
for child in get_children():
if child is ColorVariant:
cv_lists[child.type].append(child)
func set_appearance(appearance) -> void:
set_color(ColorVariant.Types.HAIR, appearance.hair_color)
set_color(ColorVariant.Types.SKIN, appearance.skin_color)
set_color(ColorVariant.Types.EYES, appearance.eye_color)
static func random_color(type : ColorVariant.Types) -> String:
var lst
match(type):
ColorVariant.Types.HAIR: lst = ColorVariant.hair_colors
ColorVariant.Types.SKIN: lst = ColorVariant.skin_colors
ColorVariant.Types.EYES: lst = ColorVariant.eye_colors
var max = 0
for opt in lst.keys():
max += lst[opt].weight
var pick = randi_range(1, max)
for opt in lst.keys():
if lst[opt].weight == 0: #Zero weighted colors are special options.
continue
pick -= lst[opt].weight
if pick < 0:
return opt
return "ERROR"
func set_color(type : ColorVariant.Types, color : String) -> void:
for cv : ColorVariant in cv_lists[type]:
cv.set_color(color)

View File

@@ -27,6 +27,11 @@ func setup(adv : Adventurer) -> void:
for child : ItemSlot in %InventoryGrid.get_children():
item_slots.append(child)
child.display_item.connect(_on_display_item)
if data.job:
var portrait : AdventurerPortrait = data.job.portrait.instantiate()
%PortraitFrame.add_child(portrait)
portrait.scale = Vector2(.6,.6)
portrait.set_appearance(data.appearance)
#TODO: Show equipment
update_items()

View File

@@ -1 +1,135 @@
extends TextureRect
class_name ColorVariant extends TextureRect
enum Types{
SKIN,
HAIR,
EYES
}
static var eye_colors = {
"blue":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(l)fair.tres"),
},
"green":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)green.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(l)default.tres"),
},
"brown":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"gold":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"red":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"grey":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
}
static var hair_colors = {
"blue":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"blonde":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/hair/(c)blonde.tres"),
"luminosity": preload("res://external/test portrait/gradients/hair/(l)fair.tres"),
},
"brown":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/hair/(c)brown.tres"),
"luminosity": preload("res://external/test portrait/gradients/hair/(l)dark.tres"),
},
"black":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/hair/(c)black.tres"),
"luminosity": preload("res://external/test portrait/gradients/hair/(l)black.tres"),
},
"red":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/hair/(c)red.tres"),
"luminosity": preload("res://external/test portrait/gradients/hair/(l)red.tres"),
},
"silver":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
}
static var skin_colors = {
"pale":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"medium":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"olive":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"brown":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"dark":{
"weight": 100,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(l)default.tres"),
},
"white":{
"weight": 0,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
"red":{
"weight": 0,
"color": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
"luminosity": preload("res://external/test portrait/gradients/eyes/(c)blue.tres"),
},
}
@export var type : Types
#@onready var variant_material : ShaderMaterial = material
func set_color(color : String) -> void:
var col_gradients
match(type):
Types.SKIN:
if !skin_colors.has(color):
printerr("Tried to set a color '%s' that isn't part of the skin color list!" % color)
return
col_gradients = skin_colors[color]
Types.HAIR:
if !hair_colors.has(color):
printerr("Tried to set a color '%s' that isn't part of the hair color list!" % color)
return
col_gradients = hair_colors[color]
Types.EYES:
if !eye_colors.has(color):
printerr("Tried to set a color '%s' that isn't part of the eye color list!" % color)
return
col_gradients = eye_colors[color]
var mat = material
mat.set_shader_parameter("color_gradient",col_gradients.color)
mat.set_shader_parameter("luminosity_gradient",col_gradients.luminosity)

View File

@@ -72,3 +72,54 @@ func notice(msg : String, time : float = 1) -> void:
func calculate_kill_exp(killer : QuestSprite, killed : QuestSprite) -> int:
return clamp(1, (killed.level - killer.level) * 5, 100)
func test_save() -> void:
var image : Image = get_viewport().get_texture().get_image()
var save_dict = {
"savetime": Time.get_datetime_string_from_system(),
"screenshot": image.save_png_to_buffer().hex_encode()
}
#Save the guild data
save_dict["guildname"] = Guild.name
save_dict["guildlevel"] = Guild.level
#Save the player data
save_dict["playername"] = player.data.full_name()
save_dict["playerlevel"] = player.data.level
#Save the employee data
#Save the adventurer data
#Save the quest data
#Save the quest progress
var save_file = FileAccess.open("user://savefile.save", FileAccess.WRITE)
save_file.store_line(JSON.stringify(save_dict))
func get_savefile_data(filename : String) -> Dictionary:
var load_file = FileAccess.open("user://" + filename, FileAccess.READ)
var json = JSON.new()
var json_string = load_file.get_line()
var parse_result = json.parse(json_string)
if not parse_result == OK:
printerr("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line())
return {}
var image = Image.new()
var ss : String = json.data.screenshot
#print(ss.data)
image.load_png_from_buffer(ss.hex_decode())
var data_dict = {
"playername": json.data.playername,
"playerlevel": json.data.playerlevel,
"guildname": json.data.guildname,
"guildlevel": json.data.guildlevel,
"savetime": json.data.savetime,
"screenshot": image
}
return data_dict
func test_load(filename : String) -> void:
var load_file = FileAccess.open("user://" + filename, FileAccess.READ)
var json = JSON.new()
var json_string = load_file.get_line()
var parse_result = json.parse_string(json_string)
if not parse_result == OK:
printerr("JSON Parse Error: ", json.get_error_message(), " in ", json_string, " at line ", json.get_error_line())
return

View File

@@ -7,7 +7,7 @@ var quest : Quest
func generate_quest() -> void:
quest = Quest.new()
quest.name = "A Test Quest"
quest.location = "Nestor Woods"
quest.location = Quest.Locations.NESTORS_WOODS
quest.difficulty = 1
quest.length = 60
quest.rewards = {"exp":100,"gold":1}
@@ -16,7 +16,7 @@ func update_quest_window() -> void:
if quest:
%NameField.text = quest.name
%DifficultyField.text = quest.difficulty_name()
%LocationField.text = quest.location
%LocationField.text = quest.location_name()
#for reward in quest.rewards.:
func reset() -> void:

View File

@@ -21,6 +21,19 @@ func setup(itm : Item) -> void:
item_grade.text = item.grade
primary_stat.text = item.primary_stat()
#TODO: Implement item secondary stats meaningfully
var ss_string = ""
var ss_count = 0
var pp_list : Array = item.stats.get_property_list()
print(item.stats)
print(item.stats.INT)
for stat in pp_list.slice(9, len(pp_list)-1):
var val = item.stats.get(stat.name)
if val:
ss_string += stat.name + " " + ("+" if val > 0 else "")
ss_string += str(val) + ("\n" if ss_count % 2 else " ")
ss_count += 1
if ss_count > 0:
secondary_stats.text = ss_string
brief.text = item.brief
func _on_drag_region_gui_input(event: InputEvent) -> void:

19
scripts/loader_screen.gd Normal file
View File

@@ -0,0 +1,19 @@
extends Control
func _ready() -> void:
var data = Game.get_savefile_data("savefile.save")
%GuildName.text = data.guildname
%GuildLevel.text = "Lv" + str(int(data.guildlevel))
%PlayerName.text = data.playername
%PlayerLevel.text = "Lv" + str(int(data.playerlevel))
%SaveTime.text = data.savetime
%Screenshot.texture = ImageTexture.create_from_image(data.screenshot)
func _on_continue_button_pressed() -> void:
Game.test_load("savefile.save")
func _on_cancel_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/start_menu.tscn")

View File

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

View File

@@ -14,6 +14,8 @@ func _ready() -> void:
func _process(delta: float) -> void:
time_changed.emit(timer.time_left)
if Input.is_action_just_pressed("save"):
Game.test_save()
func add_quest_progress_bar(quest : Quest) -> void:
var qpb : QuestProgressBar = quest_progress_bar_template.instantiate()

View File

@@ -1,258 +0,0 @@
class_name Quest extends Object
enum Status{
OPEN,
TAKEN,
IN_PROGRESS,
COMPLETED,
FAILED,
CLOSED
}
class Event:
var enemy_types: Dictionary[String, PackedScene] = {
"goo": preload("res://templates/enemies/goo.tscn")
}
enum Type{
WAIT,
COMBAT,
CHOICE
}
enum CombatState{
FIGHTING,
VICTORY,
DEFEAT
}
var type : Type = Type.WAIT
var enemies : Array[String] = []
var time : float = 1
var time_elapsed
signal completed()
signal failed()
var participants : Array = []
var turn_queue : Array = []
var busy_list : Array = []
var combat_state
var dex_speed : int
func start(quest : Quest) -> void:
match(type):
Type.WAIT:
return
Type.COMBAT:
combat_state = CombatState.FIGHTING
var enemy_list = []
for enemy_name in enemies:
enemy_list.append(enemy_types[enemy_name].instantiate())
quest.questview.set_questor_animation("idle")
for enemy in enemy_list:
quest.questview.pause_setting()
quest.questview.place_enemy(enemy)
quest.questview.set_enemy_animation(enemy, "idle")
start_combat([quest.questor.quest_sprite], enemy_list)
func start_combat(adventurers : Array, enemies : Array) -> void:
participants = []
participants.append_array(adventurers)
participants.append_array(enemies)
var c_order : Array = []
var dex_speed = 0
for p in participants:
c_order.append([p, p.stats.DEX])
if p.stats.DEX > dex_speed:
dex_speed = p.stats.DEX
c_order.sort_custom(func(a,b): return a[1] > b[1])
var delay = 5
var last_time = 0
for c in c_order:
c[0].busy.connect(_on_busy.bind(c[0]))
c[0].action_complete.connect(_on_combat_action_complete.bind(c[0]))
c[0].died.connect(_on_death.bind(c[0]))
var time = delay * dex_speed / c[1]
turn_queue.append({"combatant":c[0], "time": time - last_time})
last_time = time
func execute_attack(combatant : QuestSprite, target : QuestSprite) -> void:
var tween = combatant.create_tween()
tween.tween_interval(.25)
tween.tween_callback(combatant.attack.bind(target))
func add_to_turn_queue(combatant) -> void:
#Calculate time
var time = dex_speed / combatant.stats.DEX
#Walk through list to find insertion point
var idx = -1
for i in range(len(turn_queue)):
if turn_queue[i].time > time:
idx = i
break
else:
time -= turn_queue[i].time
var entry = {"combatant":combatant, "time":time}
if idx == -1:
turn_queue.append(entry)
else:
turn_queue[idx].time -= time
turn_queue.insert(idx,entry)
func _on_busy(combatant : QuestSprite) -> void:
busy_list.append(combatant)
func _on_death(killer : QuestSprite, combatant : QuestSprite) -> void:
busy_list.erase(combatant)
remove_from_queue(combatant)
participants.erase(combatant)
if killer != combatant:
var xp = Game.calculate_kill_exp(killer, combatant)
print("%s has earned %d exp" % [killer.name, xp])
killer.exp += xp
var enemy_list : Array = get_enemy_list(killer)
if len(enemy_list) == 0:
if killer is QuestorSprite:
victory()
else:
defeat()
func victory():
print("Questor won!")
combat_state = CombatState.VICTORY
for p : QuestorSprite in participants:
p.check_levelup()
#TODO: Notify player if level up occurs
time = 10
func defeat():
print("Questor lost!")
combat_state = CombatState.DEFEAT
failed.emit()
func remove_from_queue(combatant : QuestSprite) -> void:
var idx = -1
for i in range(len(turn_queue)):
if turn_queue[i].combatant == combatant:
idx = i
break
if idx != -1:
turn_queue.remove_at(idx)
else:
printerr("Tried to remove someone not in the turn queue")
func _on_combat_action_complete(requeue : bool, combatant : QuestSprite) -> void:
busy_list.erase(combatant)
if requeue:
add_to_turn_queue(combatant)
func execute_action(combatant) -> void:
busy_list = [combatant]
#TODO: Come up with other options than just swinging at each other
var enemies : Array = get_enemy_list(combatant)
var target = enemies.pick_random()
execute_attack(combatant, target)
func get_enemy_list(combatant) -> Array:
var lst = []
for p in participants:
if p != combatant:
lst.append(p)
return lst
func resolve_combat() -> void:
pass
func process(delta : float) -> void:
#TODO: Make quest combat work
match(type):
Type.COMBAT:
match(combat_state):
CombatState.FIGHTING:
if len(busy_list) < 1:
if len(turn_queue) > 0:
turn_queue[0].time -= delta
if turn_queue[0].time <= 0:
var c = turn_queue.pop_front()
print("%s taking a turn!" % [c.combatant.name])
if len(turn_queue) > 0:
turn_queue[0].time += c.time
execute_action(c.combatant)
else:
resolve_combat()
CombatState.VICTORY:
time_elapsed += delta
if time_elapsed >= time:
completed.emit()
Type.WAIT:
time_elapsed += delta
if time_elapsed >= time:
completed.emit()
var name : String = "A Basic Quest"
var desc : String = "The default quest, with no special anything."
var difficulty : int = 1
var location : String
var steps : int = 1
var rewards : Dictionary
var length : float = 10
var events : Array[Event] = []
var progress : float = 0
var current_step : int = 0
var taken : bool = false
var status : Status = Status.OPEN
var questview : QuestView = null
var questor : Adventurer = null
signal status_changed(status : Status)
func _init() -> void:
pass
func initiate(member : Adventurer) -> void:
questor = member
status = Status.TAKEN
status_changed.emit(Status.TAKEN)
func fail() -> void:
status = Status.FAILED
status_changed.emit(Status.FAILED)
func complete() -> void:
status = Status.COMPLETED
status_changed.emit(Status.COMPLETED)
for reward in rewards.keys():
if reward == "gold":
questor.gain_gold(rewards[reward])
elif reward == "exp":
questor.gain_exp(rewards[reward])
#TODO: Implement other reward types
#elif rewards[reward] is Item:
# questor.gain_item()
#else it's a guild item they'll bring back for us
Game.notice("%s completed the quest '%s'!" % [questor.full_name(), name])
func num_events() -> int:
return len(events)
#TODO: Put in quest requirements
func is_eligible(member : Adventurer) -> bool:
return !taken
func is_taken() -> bool:
return status == Status.TAKEN
func difficulty_name() -> String:
match(difficulty):
0: return "None"
1: return "Trivial"
2: return "Moderate"
3: return "Severe"
4: return "Extreme"
5: return "Legendary"
_: return "Unknown"

View File

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

View File

@@ -16,7 +16,7 @@ func setup(qst : Quest) -> void:
func update() -> void:
nameLabel.text = quest.name
difficultyLabel.text = quest.difficulty_name()
locationLabel.text = quest.location
locationLabel.text = quest.location_name()
#TODO: Show the current status of the quest
func close() -> void:

View File

@@ -0,0 +1,12 @@
extends Sprite2D
func _ready():
var move_tween = create_tween()
move_tween.tween_property(self, "position:y", -40, 1.25)
var fade_tween = create_tween()
modulate.a = 0
fade_tween.tween_property(self, "modulate:a", 1, .15)
fade_tween.tween_interval(0.85)
fade_tween.tween_property(self, "modulate:a", 0, .25)
fade_tween.tween_callback(queue_free)

View File

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

View File

@@ -1,7 +1,8 @@
class_name QuestorSprite extends QuestSprite
@onready var levelup_banner_template = preload("res://templates/levelup_banner.tscn")
var data : Adventurer = null
var banner_lag : float
func _ready() -> void:
if data:
@@ -14,6 +15,10 @@ func _ready() -> void:
stats = StatBlock.copy(data.stats)
gold = data.gold
func _process(delta) -> void:
if banner_lag > 0:
banner_lag -= delta
func set_animation(anim_name : String) -> void:
anim_player.play(anim_name)
@@ -28,6 +33,23 @@ func setup(adv : Adventurer) -> void:
stats = StatBlock.copy(data.stats)
gold = data.gold
adv.quest_sprite = self
data.levelled.connect(_on_level_up)
func show_levelup_banner(lagged : bool = false) -> void:
if !lagged and banner_lag > 0:
var tween = create_tween()
tween.tween_interval(banner_lag)
tween.tween_callback(show_levelup_banner.bind(true))
banner_lag += .75
return
var banner = levelup_banner_template.instantiate()
banner.position = Vector2.ZERO
$BannerOffset.add_child(banner)
banner_lag = .75
func check_levelup() -> void:
data.gain_exp(exp)
func _on_level_up() -> void:
show_levelup_banner()

View File

@@ -1,5 +1,12 @@
extends Control
func _on_button_pressed() -> void:
func _ready() -> void:
%ContinueButton.disabled = !FileAccess.file_exists("user://savefile.save")
func _on_start_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/active_scene.tscn")
func _on_continue_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/loader_screen.tscn")

58
scripts/void_ripple.gd Normal file
View File

@@ -0,0 +1,58 @@
@tool
extends Node2D
@onready var center : Node2D = $Center
@onready var outline : Line2D = $Line2D
@export var cycle_max : float = 30.0
@export var ripple_num : int = 3
@export var speed : float = 200
@export var ripple_width : float = 3
@export var gradient : Gradient
var l_norms : Array[Vector2] = []
func _ready():
recalc_lnorms()
func _process(delta: float) -> void:
queue_redraw()
print("test")
func recalc_lnorms():
var polypoints : PackedVector2Array
polypoints = outline.points.slice(0, -1)
var left_neighbor : Vector2
var right_neighbor : Vector2
#print("Recalc set")
for i in range(len(outline.points)):
var p = outline.points[i]
if i == 0:
left_neighbor = outline.points[-2]
else:
left_neighbor = outline.points[i-1]
if i == len(outline.points) - 1:
right_neighbor = outline.points[1]
else:
right_neighbor = outline.points[i+1]
var shift : Vector2 = ((left_neighbor - p).normalized() + (right_neighbor - p).normalized()).normalized()
if Geometry2D.is_point_in_polygon(p + shift, polypoints):
#print("Offset moves the point into the polygon, flip!")
shift *= -1
l_norms.append(shift)
func _draw() -> void:
var ripple_line : PackedVector2Array
ripple_line.resize(len(outline.points))
var min : float = -cycle_max / ripple_num
var max : float = cycle_max + min
var cycle = wrapf(Time.get_ticks_msec()*speed, min, max)
for j in range(ripple_num):
var color = gradient.sample((cycle / cycle_max))
for i in len(ripple_line):
ripple_line[i] = outline.points[i] + l_norms[i] * cycle
#print(str(outline.points[i]) + " becomes " + str(ripple_line[i]) + " because of offset " + str(l_norms[i]))
draw_polyline(ripple_line, Color(color, .8 * (cycle_max - cycle) / cycle_max), ripple_width * (cycle_max - cycle) / cycle_max, true)
cycle = wrapf(cycle + cycle_max / ripple_num, min, max)
pass

View File

@@ -0,0 +1 @@
uid://2g4ja3a7o2l