class_name Princess extends CharacterBody2D ## The player listens for input actions appended with this suffix.[br] ## Used to separate controls for multiple players in splitscreen. @export var action_suffix := "" var gravity: int = ProjectSettings.get("physics/2d/default_gravity") @onready var wall_detect_left := $wall_detect_left as RayCast2D @onready var wall_detect_right := $wall_detect_right as RayCast2D @onready var wall_detect_left2 := $wall_detect_left2 as RayCast2D @onready var wall_detect_right2 := $wall_detect_right2 as RayCast2D @onready var wall_detect_left3 := $wall_detect_left3 as RayCast2D @onready var wall_detect_right3 := $wall_detect_right3 as RayCast2D @onready var wall_far_detect_left := $wall_far_detect_left as RayCast2D @onready var wall_far_detect_left2 := $wall_far_detect_left2 as RayCast2D @onready var wall_far_detect_left3 := $wall_far_detect_left3 as RayCast2D @onready var wall_far_detect_right := $wall_far_detect_right as RayCast2D @onready var wall_far_detect_right2 := $wall_far_detect_right2 as RayCast2D @onready var wall_far_detect_right3 := $wall_far_detect_right3 as RayCast2D @onready var ground_far_detect := $ground_far_detect as RayCast2D @onready var ground_far_detect2 := $ground_far_detect2 as RayCast2D @onready var animation := $AnimatedSprite2D as AnimatedSprite2D @onready var camera := $Camera2D as Camera2D @onready var death_animation := $"Death player" as AnimationPlayer @onready var nuage_prout := $"GPUParticles2D" as GPUParticles2D @onready var nuage_jump := $"GPUParticles2DJump" as GPUParticles2D ################################################################################ # # Constantes de déplacement à pimper dans l'inspecteur # ################################################################################ @export var WALKING_SPEED = 155 @export var FALLING_SPEED = 230 @export var JUMPING_SPEED = 220 var DASH_SPEED = WALKING_SPEED * 2 @export var X_SPEED_TABLE = [0, 0.1, 0.15, 0.3, 0.4, 0.7, 1] @export var X_SPEED_DECEL = [0, 0.1, 0.6, 1] @export var FALL_SPEED_TABLE = [0, 0.1, 0.15, 0.2, 0.3, 0.6, 0.9, 1] @export var JUMP_SPEED_TABLE = [0.2, 0.3, 0.5, 0.6, 0.7, 0.8, 0.9, 1] @export var KICK_SPEED_TABLE = [0.5, 0.6, 0.7, 0.8, 0.9, 1.1] @export var DASH_SPEED_TABLE = [0.5, 0.6, 0.7, 0.8, 0.9, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3] # Nombre d'incrément à rajouter lorsque la touche de saut est maintenue @export var JUMPING_COUNTER_REFILL = 2 # Tous les combien d'incréments rajouter un refill sur le compteur @export var JUMPING_KEY_COUNTER_THRESHOLD = 3 @export var KICK_JUMP_LIMITER = 0.75 @export var KICK_REFILL_MAX_AMOUNT = 10 # Nombre de frames coyote durant lesquelle le joueur peut encore sauter # sans encore être au sol @export var COYOTE_LENGTH := 5 @export var COYOTE_GRAB_LENGTH := 15 # Différence de vitesse de ralentissement entre le sol et l'air @export var WALK_INCR_GROUND : int = 1 @export var WALK_INCR_AIR : int = 3 @export var MAX_FARTS : int = 3 signal cheese_collected(speed:float) # utilisé pour remplir le HUD qui apelera # ensuite la fonction reload_fart quand il aura fini signal lactase_collected() # utilisé pour vider le HUD qui apelera # ensuite la fonction kill_farts quand il aura fini signal dash_fart() # utilisé pour vider le HUD signal princesse_is_dead() # signal au jeu de recharger la scene var locked = true var locked_controls = true ################################################################################ # # Etat de la princesse # ################################################################################ var dead = false # Direction dans laquelle est positioné le personnage. var direction : int = 1 var walking : bool = false # si le joueur veut marche ou non var walking_step: int = -1 # où la princesse en est dans son tableau d'accel var walk_incr_reserve : int = 0 # utilisé pour la différence ralentissement air/sol var init_decel: bool = true # utilisé pour initialiser la décélération var init_direction_change = true # utilisé pour initialiser le changement de direction var near_cliff = false # variables d'état relatives au saut var jumping : bool = false # Princesse est-elle en train de sauter ? var need_jump : bool = false # Le joueur veut-il sauter et peut il ? var jumping_step : int = -1 # Où en est la princesse dans son tableau d'accel var jump_key_counter : int = 0 # Où en est-on du refil ? var collide_ground_far_detect = false # est-ce que les collisioneurs du sol sont en contact avec ? # variables d'état relatives au kick mural var collide_wall_far_detect = false # est-ce que les collisioneurs du sol sont en contact avec ? var need_kicking : bool = false # Le joueur veut-il sauter et peut il ? var kicking : bool = false var kick_step : int = -1 var kick_direction : int = 1 var last_kick_key_counter : int = 0 # Coyote time var coyote_ground = [] var coyote_grab = [] # variable d'état relative à la gravité var falling_step : int = -1 # Où en est la princess dans son acceleration de chute ? # Variables d'état relative à l'accroche au mur var pressing_wall_left = false # Princesse est elle en contact avec un mur à gauche? var pressing_wall_right = false # Princesse est elle en contact avec un mur droite ? var pressing_wall = false # Princesse est elle en contact avec un mur ? var grab_wall = false # Je joueur veut-il et peut-il s'accrocher au mur ? # Variables d'état relative au dash var dashing : bool = false var dash_step : int = -1 var dash_direction_x : int = 1 var dash_direction_y : int = 1 # Variables d'état relative au crouch var crouch : bool = false func copy_from(other : Princess): direction = other.direction walking = false walking_step = other.walking_step walk_incr_reserve = other.walk_incr_reserve init_decel = other.init_decel init_direction_change = other.init_direction_change jumping = other.jumping need_jump = other.need_jump need_kicking = other.need_kicking jumping_step = other.jumping_step jump_key_counter = other.jump_key_counter kicking = other.kicking kick_step = other.kick_step kick_direction = other.kick_direction coyote_ground = other.coyote_ground coyote_grab = other.coyote_grab falling_step = other.falling_step pressing_wall_left = other.pressing_wall_left pressing_wall_right = other.pressing_wall_right pressing_wall = other.pressing_wall grab_wall = other.grab_wall dashing = other.dashing dash_step = other.dash_step dash_direction_x = other.dash_direction_x dash_direction_y = other.dash_direction_y near_cliff = other.near_cliff ################################################################################ # # Gestion d'avec quoi Princesse collisionne # ################################################################################ const PLATFORM_LAYER = 1 << (5 - 1) # collision layer 5 -> plateformes const PICS_BLOCK_LAYER = 1 << (6 - 1) # collision layer 6 -> pics const CHEESE_LAYER = 1 << (6 - 1) # collision layer 7 -> fromages var layer_of_collision = null func init_walk_state(): walk_incr_reserve = 0 walking_step = -1 init_decel = false func walk(direction:int) -> float: # Fait marcher le personnage # Le personnage ne se déplace pas dans les cas suivants: if kicking or kicking or dashing or get_coyote(coyote_grab): init_walk_state() return velocity.x if not get_coyote(coyote_grab) and pressing_wall: return WALKING_SPEED * direction *1.2 # Si le personnage est dans l'air, il aura une friction plus faible lors de # la décélération. var threshold = WALK_INCR_AIR if is_on_floor(): threshold = WALK_INCR_GROUND # Un changement de direction implique de perdre la vélocité dans la direction # précédente avant de repartir dans la direction que l'on veut. # ça ne change rien pour le nombre de frames nécéssaires pour accélérer dans # tous les cas. Mais le feeling est meilleur. var direction_change: bool = (direction > 0 and velocity.x < 0) or (direction < 0 and velocity.x > 0) if direction_change: if init_direction_change: init_direction_change = false walk_incr_reserve += 1 # appliquer le nombre de frames nécéssaire pour décrémenter, dépend # de où est le personnage (air vs sol) if walk_incr_reserve >= threshold: walk_incr_reserve = 0 walking_step-=1 else: init_direction_change = true # Si le joueur décide de marcher alors, le compteur de pas doit s'incrémenter # Si le joueur ne veut plus marcher, alors, le compteur de pas décrémente # La vitese choisie est le numéro d'étape dans le tableau correspondant # X_SPEED_TABLE pour l'accélération # X_SPEED_DECEL pour la décélération var table = X_SPEED_TABLE if not walking: table = X_SPEED_DECEL # dans le cas où le personnage est propulsé par une force extérieure et # qu'il n'y a pas d'input du joueur. Lorsque le joueur va vouloir reprendre # la main, il faut démarrer à la bonne vélocité var abs_v_x = abs(velocity.x) if abs_v_x != 0: if walking_step == -1: # trouver l'indice le plus proche de la vitesse courante for index in range(0, table.size()-1): var speed_i = table[walking_step] * WALKING_SPEED var speed_i1 = table[walking_step+1] * WALKING_SPEED # lorsque l'on a trouvé l'indice le bon endroit dans le tableau, alors # on renvoie l'indice trouvé if abs_v_x == speed_i: walking_step = index if abs_v_x == speed_i1: walking_step = index+1 if abs_v_x > speed_i and abs_v_x < speed_i1: walking_step = index+1 # si rien n'est trouvé, alors on initialise l'indice au maximum possible if walking_step == -1: walking_step = table.size() - 1 if walking: walk_incr_reserve += 1 # appliquer le nombre de frames nécéssaire pour décrémenter, dépend # de où est le personnage (air vs sol) if walking_step > 2: if walk_incr_reserve >= threshold: walk_incr_reserve = 0 walking_step = min(walking_step+1, X_SPEED_TABLE.size() -1) else: walking_step+=1 init_decel = true else: # Lors de la première frame de la décélération, initialiser la valeur # du compteur d'incrément en haut du tableau de décélération if init_decel: walking_step = min(walking_step, X_SPEED_DECEL.size() - 1) init_decel = false if near_cliff: walking_step = 0 # Si le compteur d'incrément est supérieur ou égal à zéro, c'est qu'il # faut bouger, donc il est nécéssaire en premier temps de récupérer la vitesse # à l'indice courant. Puis si le joueur ne veut plus accélérer, alors, appliquer # la décélération if walking_step >= 0: var speed = table[walking_step] * WALKING_SPEED if not walking: walk_incr_reserve += 1 # appliquer le nombre de frames nécéssaire pour décrémenter, dépend # de où est le personnage (air vs sol) if walk_incr_reserve >= threshold: walk_incr_reserve = 0 walking_step-=1 if direction_change and walking: # dans le cas du changement de direction, return speed * direction + velocity.x else: return speed * direction return velocity.x func fall() -> float: # fait tomber princesse if jumping or kicking or dashing: stop_fall() return velocity.y var intertia = 0 if velocity.y < 0: intertia = max(velocity.y, -FALLING_SPEED) if is_on_floor_only(): if get_floor_normal()[0] < 0: # pente à gauche if direction >= 0:# on va à droite, désactive la gravité falling_step = -1 return velocity.y else: # on va à gauche, gravité à fond falling_step = FALL_SPEED_TABLE.size()-1 else: # pente à droite if direction > 0:# on va à droite, active la gravité à fond falling_step = FALL_SPEED_TABLE.size()-1 else: # on va à gauche, désactive la gravité falling_step = -1 return velocity.y else: if get_coyote(coyote_grab) and not crouch: falling_step = max(falling_step-1, 1) else: if intertia < 0 and intertia + FALL_SPEED_TABLE[falling_step] * FALLING_SPEED >0: falling_step = 0 falling_step = min(falling_step+1, FALL_SPEED_TABLE.size()-1) return intertia + FALL_SPEED_TABLE[falling_step] * FALLING_SPEED func stop_fall() -> void: falling_step = -1 func jump() -> float: # fait sauter princesse if not jumping or kicking or dashing: return velocity.y if not is_on_ceiling() and jump_key_counter > 0 and jump_key_counter % JUMPING_KEY_COUNTER_THRESHOLD == 0: jumping_step = min( jumping_step + JUMPING_COUNTER_REFILL, JUMP_SPEED_TABLE.size() -1 ) if jumping_step > 0: jumping_step -= 1 return JUMP_SPEED_TABLE[jumping_step] * JUMPING_SPEED * -1 else: end_jump() return velocity.y func end_jump(): # termine le saut de Princesse jumping = false jumping_step = -1 func kick() -> void: # fait kicker la princesse if not kicking or dashing: return if not is_on_ceiling() and jump_key_counter > last_kick_key_counter and jump_key_counter < KICK_REFILL_MAX_AMOUNT : last_kick_key_counter = jump_key_counter kick_step = KICK_SPEED_TABLE.size() -1 else: last_kick_key_counter = 1000 if kick_step > 0: kick_step -= 1 velocity.y = KICK_SPEED_TABLE[kick_step] * JUMPING_SPEED * -1 * KICK_JUMP_LIMITER velocity.x = KICK_SPEED_TABLE[kick_step] * WALKING_SPEED * kick_direction else: cancel_kick() func cancel_kick() -> void: kick_step = -1 kicking=false func dash() -> void: # fait dasher la princesse if not dashing: return if dash_step > 0: dash_step -= 1 velocity.y = DASH_SPEED_TABLE[dash_step] * DASH_SPEED * dash_direction_y velocity.x = DASH_SPEED_TABLE[dash_step] * DASH_SPEED * dash_direction_x else: cancel_dash() func cancel_dash() -> void: dash_step = -1 dashing=false nuage_prout.emitting = false func choose_animation_orientation() -> void: # Oriente l'animation correctement en fonction de laquelle on joue et de # la direction du personnage if not is_zero_approx(velocity.x): if velocity.x > 0.0: animation.scale.x = 1.0 else: animation.scale.x = -1.0 if get_coyote(coyote_grab): if pressing_wall_left: animation.scale.x = 1 elif pressing_wall_right: animation.scale.x = -1 func play_animation() -> void: # Joue l'animation de Princesse et dans le bon sens choose_animation_orientation() var anim := get_new_animation() if anim != animation.animation: animation.animation = anim animation.play() func move_and_handle_collisions() -> void: if kicking or dashing: end_jump() if dashing: cancel_kick() # Bouge Princesse et réagis aux éléments avec lesquels elle rentre en collision move_and_slide() for i in get_slide_collision_count(): var collider : KinematicCollision2D= get_slide_collision(i) var tile_rid = collider.get_collider_rid() layer_of_collision = PhysicsServer2D.body_get_collision_layer(tile_rid) if layer_of_collision == PICS_BLOCK_LAYER: death() func get_coyote(table: Array): var result = false for i in table: result = result or i return result func read_input() -> void: # Lis les commandes du joueur pour piloter Princesse if locked or locked_controls: return # le joueur veut-il accélérer sa chute si il s'accroche au mur? crouch = Input.is_action_pressed("move_down" + action_suffix) # Le joueur veut-il sauter ou kicker ? if Input.is_action_just_pressed("jump" + action_suffix): # Peut-il sauter ? if not kicking and collide_ground_far_detect or (is_on_floor() or get_coyote(coyote_ground)): need_jump=true need_kicking=false else: need_jump=false # Peut-il kicker ? if not kicking and (collide_wall_far_detect or get_coyote(coyote_grab)): need_kicking=true need_jump=false else: need_kicking=false # Dès qu'il peut et veut kicker, déclencher le kick if need_kicking and get_coyote(coyote_grab): if not kicking: need_kicking=false nuage_jump.restart() nuage_jump.emitting = true kicking=true coyote_grab = [false] last_kick_key_counter = 0 kick_step = KICK_SPEED_TABLE.size()-1 if pressing_wall_left: kick_direction = 1 else: kick_direction = -1 # Dès qu'il peut et veut sauter, déclencher le saut if need_jump and (is_on_floor() or get_coyote(coyote_ground)): need_jump = false if not jumping: nuage_jump.restart() nuage_jump.emitting = true jumping = true jumping_step = JUMP_SPEED_TABLE.size()-1 # Si il continue d'appuyer, lui rallonger son saut if Input.is_action_pressed("jump" + action_suffix): jump_key_counter += 1 else: jump_key_counter = 0 # Le joueur veut-il marcher ? walking = ( Input.is_action_pressed("move_left" + action_suffix) or Input.is_action_pressed("move_right" + action_suffix) ) # Calculer dans quelle direction il veut marcher if walking: var axis = Input.get_axis( "move_left" + action_suffix, "move_right" + action_suffix ) if not is_zero_approx(axis): if axis < 0: if direction == 1 and is_on_floor(): nuage_jump.restart() nuage_jump.emitting = true direction = -1 else: if direction == -1 and is_on_floor(): nuage_jump.restart() nuage_jump.emitting = true direction = 1 else: if kicking: direction = kick_direction # Le joueur veut-il et peut-il s'accrocher au mur ? if pressing_wall: if not grab_wall : grab_wall = ( (pressing_wall_left and direction == -1) or (pressing_wall_right and direction == 1) ) else: # Désactivation du grab wall if walking: if pressing_wall_left and direction == 1: grab_wall = false if pressing_wall_right and direction == -1: grab_wall = false if kicking or dashing: grab_wall = false else: grab_wall = false coyote_grab = [false] if Input.is_action_just_pressed("dash" + action_suffix): if not dashing: var axis_x = Input.get_axis( "move_left" + action_suffix, "move_right" + action_suffix ) var axis_y = Input.get_axis( "move_up" + action_suffix, "move_down" + action_suffix ) if not is_zero_approx(axis_x): if axis_x < 0: dash_direction_x = -1 else: dash_direction_x = 1 else: dash_direction_x = 0 if not is_zero_approx(axis_y): if axis_y < 0: dash_direction_y = -1 else: dash_direction_y = 1 else: dash_direction_y = 0 # Ne dasher que si une direction est donnée avec le joystick if dash_direction_x or dash_direction_y: Input.start_joy_vibration(0, 1, 0.5, 0.2) dashing = true nuage_prout.restart() nuage_prout.emitting = true dash_fart.emit() # Limiter le dash en diagonnale car sinon il est trop grand # par rapport à un dash dans une seule direction if dash_direction_x and dash_direction_y: dash_step = DASH_SPEED_TABLE.size()-2 else: dash_step = DASH_SPEED_TABLE.size()-1 func compute_state() -> void: collide_ground_far_detect = ground_far_detect.is_colliding() or ground_far_detect2.is_colliding() near_cliff = collide_ground_far_detect and not(ground_far_detect.is_colliding() and ground_far_detect2.is_colliding()) collide_wall_far_detect = ( wall_far_detect_left.is_colliding() or wall_detect_left2.is_colliding() or wall_detect_left3.is_colliding() or wall_far_detect_right.is_colliding() or wall_far_detect_right2.is_colliding() or wall_far_detect_right3.is_colliding() ) # Met à jour une partie de l'état de la princesse # gestion du coyote time sur le contact au sol coyote_ground.append(is_on_floor()) if coyote_ground.size() > COYOTE_LENGTH: coyote_ground.remove_at(0) # Met à jour une partie de l'état de la princesse # gestion du coyote time sur le contact au sol coyote_grab.append(grab_wall) if coyote_grab.size() > COYOTE_GRAB_LENGTH: coyote_grab.remove_at(0) # gestion de l'état de la pression au mur pressing_wall_left = ( not is_on_floor() and not collide_ground_far_detect and not is_on_ceiling_only() ) and ( (wall_detect_left.is_colliding() and wall_detect_left2.is_colliding()) or (wall_detect_left3.is_colliding()) ) pressing_wall_right = ( not is_on_floor() and not collide_ground_far_detect and not is_on_ceiling_only() ) and ( (wall_detect_right.is_colliding() and wall_detect_right2.is_colliding()) or (wall_detect_right3.is_colliding()) ) pressing_wall = pressing_wall_left or pressing_wall_right func get_new_animation() -> String: # Renvoie la bonne annimation en fonction de l'état de la princesse var animation_new: String if is_on_floor(): if walking_step > 0: animation_new = "walk" else: if near_cliff: animation_new = "near_cliff" else: animation_new = "idle" else: if velocity.y > 0.0: if get_coyote(coyote_grab): animation_new = "wall_stick" else: if walking_step > 0: animation_new = "falling_diagonals" else: animation_new = "falling_straight" else: animation_new = "jumping" return animation_new func _physics_process(_delta: float) -> void: if dead or locked: return compute_state() read_input() velocity.y = jump() velocity.y = fall() velocity.x = walk(direction) kick() dash() move_and_handle_collisions() play_animation() func you_got_cheese(speed:float=1) -> void: cheese_collected.emit(speed) reload_fart() func reload_fart() -> void: Input.start_joy_vibration(0, 0, 1, 0.2) func you_got_lactase() -> void: lactase_collected.emit() kill_farts() func kill_farts() -> void: cancel_dash() Input.start_joy_vibration(0, 0.5, 0, 0.1) func death() -> void: if not dead: print("death") locked = true locked_controls = true dead = true Input.start_joy_vibration(0, 1,0.7, 1) self.rotation = 90 death_animation.play(&"death") kill_farts() func _ready() -> void: locked = false dead = false func go_out_and_play(): locked_controls = false func _on_death_player_animation_finished(_anim_name: StringName) -> void: princesse_is_dead.emit()