This repository has been archived on 2025-09-12. You can view files and clone it, but cannot push or open issues or pull requests.
ld49/game/customer/GenericCustomer.gd

285 lines
10 KiB
GDScript

class_name GenericCustomer
extends KinematicBody
#Generic Customer script
var model_modifier:Array
onready var spots_collection:SpotsGroupList = load("res://game/customer/spots/SpotsGroupList.gd").new()
const TEXTURES: Array = [
preload("res://art_assets/customer/customerDiffuseA.png"),
preload("res://art_assets/customer/customerDiffuseB.png"),
preload("res://art_assets/customer/customerDiffuseC.png"),
]
enum states {idle, waiting_to_order, waiting_for_order, drinking, walking}
enum feelings {happy, indifferent, bored, insane}
enum possible_icons {wait_chill, wait_warning, wait_angry}
var icon_scenes: Dictionary = {
possible_icons.wait_chill:load("res://assets/Icon_Waiting_Chill.tscn"),
possible_icons.wait_warning:load("res://assets/Icon_Waiting_Warning.tscn"),
possible_icons.wait_angry:load("res://assets/Icon_Waiting_Angry.tscn")
}
onready var icon_attachment: Node = $Icon
onready var displayed_icon: int = -1
var current_state:int = states.idle
var customer_possible_difficulty = [1, 2, 3] #Difficulty will halve time to please the customer
onready var customer_difficulty = customer_possible_difficulty[randi() % customer_possible_difficulty.size()]
onready var effect_multiplier = 0.5 + 0.5 * customer_difficulty
export var max_speed:float = 10
export var waiting_time_tolerance = 100
export var happy_effect: float = 5
export var grumble_effect: float = -1
export var angry_effect: float = -5
export(float, 0.0, 3.0) var grumble_min_pitch: float = 1.0
export(float, 0.0, 3.0) var grumble_max_pitch: float = 1.2
onready var max_waiting_timer:Timer = $MaxWaitingTime
signal started_walking
signal started_idling
signal despawning(node)
signal new_movement_target(target)
var barista_took_order:bool = false
var barista_called_for_delivery:bool = false
var got_food:bool = false
var target:Spatial = null
onready var FSM:FiniteStateMachine = $FSM
onready var customer_generated_food_order = OrderRepository.generate_order(customer_difficulty, false)
var allocated_spot:Spatial = null
onready var speech_bubble = $SpeechBubble
onready var anim_tree = $AnimationTree
onready var anim_state_machine = anim_tree.get("parameters/playback")
onready var animPlayer:AnimationPlayer = $customer/AnimationPlayer
onready var model: MeshInstance = $customer/customerArmature/Skeleton/customer
var order_score = 0
#Animation timers, will make the customer wait before changing state, so the animation has time to play
onready var pickup_food_timer:Timer = $AnimationTimers/PickupFoodTimer
onready var place_order_timer:Timer = $AnimationTimers/PlaceOrderTimer
var pickup_food_order_check:bool = false
var place_order_timer_check:bool = false
var current_focus:Spatial = null
onready var focus_tween:Tween = $FocusTween
onready var waiting_time_sfx: AudioStreamPlayer3D = $WaitingTimeSfx
onready var happy_sfx: AudioStreamPlayer3D = $HappySfx
onready var sad_sfx: AudioStreamPlayer3D = $SadSfx
var rng: RandomNumberGenerator = RandomNumberGenerator.new()
func emit_started_walking()->void:
emit_signal("started_walking")
func emit_started_idling()->void:
emit_signal("started_idling")
func emit_despawning()->void:
emit_signal("despawning", self)
func _face_focus_direction(new_focus:Spatial, override_for_animation:bool = false)->void:
if new_focus == current_focus:
return
if FSM.current_state == FSM.walking and not override_for_animation:
return
if not is_instance_valid(allocated_spot):
return
if not allocated_spot.has_method("get_focus_direction"):
return
var direction:Vector3 = (allocated_spot.global_transform.origin+allocated_spot.get_focus_direction())-global_transform.origin
focus_tween.interpolate_property(self, "rotation",
rotation, Vector3(rotation.x,atan2(direction.x, direction.z),rotation.z), 0.5,
Tween.TRANS_LINEAR, Tween.EASE_IN_OUT)
focus_tween.start()
func _get_and_allocate_spot(group_name:String)->Spatial:
var seats:Array = get_tree().get_nodes_in_group(group_name)
seats.shuffle()
for i in seats:
if not i.busy:
var attempt = i.set_busy(true, self)
if attempt:
if is_instance_valid(allocated_spot) and allocated_spot.has_method("leave"):
allocated_spot.leave()
allocated_spot = i
return i
else:
return null
#Did not find spot
return null
func call_customer_to_deliver_zone()->bool:
if go_get_food_spot(): #Only true if the spot is allocated by this customer
barista_called_for_delivery = true
return true
return false
func needs_fullfilled()->void:
OrderRepository.remove_order(self)
OrderRepository.emit_client_is_satisfied(self, happy_effect * effect_multiplier)
func grumble()->void:
waiting_time_sfx.pitch_scale = rng.randf_range(grumble_min_pitch, grumble_max_pitch)
waiting_time_sfx.play()
OrderRepository.emit_client_is_unhappy(self, grumble_effect * effect_multiplier) #Customer is grumbly
func needs_failed()->void:
OrderRepository.emit_client_is_enraged(self, angry_effect * effect_multiplier) #Kept waiting forever, not cool
func deliver_order_to_barista()->void:
FSM.change_state(FSM.delivering_order_to_barista)
barista_took_order = true
OrderRepository.add_order(self, customer_generated_food_order)
speech_bubble.render_orders(customer_generated_food_order)
speech_bubble.show_bubble()
func receive_order(received_item:int)->bool: #True = the delivered item is correct
if received_item in customer_generated_food_order:
customer_generated_food_order.erase(received_item)
if customer_generated_food_order.empty():
needs_fullfilled()
speech_bubble.hide_bubble()
return true
speech_bubble.render_orders(customer_generated_food_order)
return false
func go_ask_for_food_spot()->Spatial:
if is_instance_valid(allocated_spot) and allocated_spot.is_in_group("ask_food_spot"):
return allocated_spot
target = _get_and_allocate_spot(spots_collection.spot_names[spots_collection.ask_food_spot])
move_to(target)
return target
func go_waiting_spot()->void:
if is_instance_valid(allocated_spot):
if allocated_spot.is_in_group(spots_collection.spot_names[spots_collection.waiting_spot]):
return
target = _get_and_allocate_spot(spots_collection.spot_names[spots_collection.waiting_spot])
move_to(target)
func go_get_food_spot()->bool:
if is_instance_valid(allocated_spot):
if allocated_spot.is_in_group(spots_collection.spot_names[spots_collection.get_food_spot]):
return true
target = _get_and_allocate_spot(spots_collection.spot_names[spots_collection.get_food_spot])
if is_instance_valid(target):
move_to(target)
return true
return false
func find_seat()->void:
target = _get_and_allocate_spot(spots_collection.spot_names[spots_collection.drinking_spot])
move_to(target)
func leave_and_go_away()->void:
if allocated_spot != null:
if allocated_spot.has_method("leave"):
allocated_spot.leave() #current allocated seat
var exit_spots:Array = get_tree().get_nodes_in_group(spots_collection.spot_names[spots_collection.exit_spot])
var chosen_exit:Spatial = exit_spots[randi() % exit_spots.size()]
target = chosen_exit
allocated_spot = chosen_exit
move_to(target)
OrderRepository.remove_order(self)
func _ready():
add_child(spots_collection)
animPlayer.get_animation("customerWalk").loop = true
animPlayer.get_animation("customerWaitTable").loop = true
animPlayer.get_animation("customerWaitRegister").loop = true
animPlayer.get_animation("customerDrinkIdle").loop = true
max_waiting_timer.wait_time = waiting_time_tolerance/customer_difficulty
max_waiting_timer.start()
# Clone the mesh to prevent artifacts from shape keys
var mesh: Mesh = model.mesh.duplicate()
model.mesh = mesh
# Clone the material to prevent overwriting source albedo
var mat: SpatialMaterial = mesh.surface_get_material(0).duplicate()
mat.albedo_texture = TEXTURES[randi() % TEXTURES.size()]
model.set_surface_material(0, mat)
model_modifier = [randf(), randf(), randf()]
model.set("blend_shapes/bodyThicker", model_modifier[0])
model.set("blend_shapes/hairRound", model_modifier[1])
model.set("blend_shapes/hairSharp", model_modifier[2])
func _process(delta) -> void:
# Check to make sure our shape keys are not randomly changing
assert(model.get("blend_shapes/moodBad") == 0, "%s - Customer model blend_shapes/moodBad should be 0, current value is %f" % [name, model.get("blend_shapes/moodBad")])
assert(model.get("blend_shapes/moodGood") == 0, "%s - Customer model blend_shapes/moodGood should be 0, current value is %f" % [name,model.get("blend_shapes/moodGood")])
assert(model.get("blend_shapes/bodyThicker") == model_modifier[0], "%s - Customer model blend_shapes/bodyThicker should be %f, curret value is %f" % [name, model_modifier[0], model.get("blend_shapes/bodyThicker")])
assert(model.get("blend_shapes/hairRound") == model_modifier[1], "%s - Customer model blend_shapes/hairRound should be %f, current value is %f" % [name, model_modifier[1],model.get("blend_shapes/hairRound")])
assert(model.get("blend_shapes/hairSharp") == model_modifier[2], "%s - Customer model blend_shapes/hairSharp should be %f, current value is %f" % [name, model_modifier[2], model.get("blend_shapes/hairSharp")])
func move_to(target:Spatial):
if target == null:
return
emit_signal("new_movement_target", target)
FSM.change_state(FSM.walking)
func _on_MaxWaitingTime_timeout():
match FSM.current_state:
FSM.waiting_for_order:
print_debug("Customer expired, reason: waited for order too long")
needs_failed()
leave_and_go_away()
FSM.waiting_to_order:
print_debug("Customer expired, reason: waited to order too long")
needs_failed()
leave_and_go_away()
FSM.drinking:
print_debug("Customer expired, reason: consumed drink: ", order_score)
leave_and_go_away()
FSM.idle:
print_debug("Customer expired, reason: expired while idling")
leave_and_go_away()
FSM.walking:
max_waiting_timer.start() #Reset time, was walking, dont go away in this state ever
_:
printerr("Customer tolerance time expired while he was in a unexpected state", FSM.current_state, get_stack())
#After the customer placed the order
func _on_PlaceOrderTimer_timeout():
go_waiting_spot()
func add_icon(icon_type:int) -> void:
# Remove any other icons that are currently displayed above the model
if icon_type != displayed_icon:
if icon_attachment.get_child_count() > 0:
for child in icon_attachment.get_children():
child.queue_free()
var icon_instance = icon_scenes[icon_type].instance()
icon_attachment.add_child(icon_instance)
set_displayed_icon(icon_type)
func remove_icon() -> void:
if icon_attachment.get_child_count() > 0:
for child in icon_attachment.get_children():
child.queue_free()
set_displayed_icon(-1)
func set_displayed_icon(icon_type:int):
displayed_icon = icon_type