351 lines
15 KiB
GDScript
351 lines
15 KiB
GDScript
extends Node3D
|
||
|
||
var weaponStack : Array[int] = [] #weapons current wielded by play char
|
||
var weaponList : Dictionary = {} #all weapons available in the game (key = weapon name, value = wepakn resource)
|
||
@export var weaponResources : Array[WeaponResource] #all weapon resources files
|
||
@export var startWeapons : Array[WeaponSlot] #the weapon the player character will start with
|
||
|
||
var cW = null #current weapon
|
||
var cWModel = null #current weapon model
|
||
var weaponIndex : int = 0
|
||
|
||
#weapon changes variables
|
||
var canChangeWeapons : bool = true
|
||
var canUseWeapon : bool = true
|
||
|
||
@export_group("Keybind variables")
|
||
@export var shoot_action : String
|
||
@export var reload_action : String
|
||
@export var weapon_wheel_up_action : String
|
||
@export var weapon_wheel_down_action : String
|
||
|
||
@onready var playChar : CharacterBody3D = $"../../../.."
|
||
@onready var cameraHolder : Node3D = %CameraHolder
|
||
@onready var cameraRecoilHolder : Node3D = %CameraRecoilHolder
|
||
@onready var camera : Camera3D = %Camera
|
||
@onready var weaponContainer : Node3D = %WeaponContainer
|
||
@onready var shootManager : Node3D = %ShootManager
|
||
@onready var reloadManager : Node3D = %ReloadManager
|
||
@onready var ammoManager : Node3D = %AmmunitionManager
|
||
@onready var animPlayer : AnimationPlayer = %AnimationPlayer
|
||
@onready var animManager : Node3D = %AnimationManager
|
||
@onready var audioManager : PackedScene = preload("../../Misc/Scenes/AudioManagerScene.tscn")
|
||
@onready var bulletDecal : PackedScene = preload("../../Weapons/Scenes/BulletDecalScene.tscn")
|
||
@onready var hud : CanvasLayer = %HUD
|
||
@onready var linkComponent : Node3D = %LinkComponent
|
||
|
||
func _ready():
|
||
initialize()
|
||
|
||
func initialize():
|
||
for weapon in weaponResources:
|
||
#create dict to refer weapons
|
||
weaponList[weapon.weaponId] = weapon
|
||
|
||
for weapo in weaponList.keys():
|
||
#weaponsEmplacements[weapo] = weaponIndex
|
||
cW = weaponList[weapo] #set each weapon to current, to acess properties useful to set up animations slicing and select correct weapon slot
|
||
|
||
for weaponSlot in weaponContainer.get_children():
|
||
if weaponSlot.weaponId == cW.weaponId: #id correspondant
|
||
|
||
#if weapon is in the predetermined start weapons list
|
||
for startWeapon in startWeapons:
|
||
if startWeapon.weaponId == cW.weaponId:
|
||
weaponStack.append(cW.weaponId)
|
||
|
||
cW.weaponSlot = weaponSlot #get weapon slot script ref from weapon list (allows to get access to model, attack point, ...)
|
||
cWModel = cW.weaponSlot.model
|
||
cWModel.visible = false
|
||
|
||
forceAttackPointTransformValues(cW.weaponSlot.attackPoint)
|
||
|
||
cW.bobPos = cW.position
|
||
|
||
if weaponStack.size() > 0:
|
||
#enable (equip and set up) the first weapon on the weapon stack
|
||
enterWeapon(weaponStack[0])
|
||
|
||
func exitWeapon(nextWeapon : int):
|
||
#this function manage the first part of the weapon switching mechanic
|
||
#in this part, the current weapon is disabled (unequiped and taked down)
|
||
if nextWeapon != cW.weaponId:
|
||
canChangeWeapons = false
|
||
canUseWeapon = false
|
||
if cW.isShooting: cW.isShooting = false
|
||
if cW.isReloading: cW.isReloading = false
|
||
|
||
if cW.unequipAnimName != "":
|
||
animManager.playAnimation("UnequipAnim%s" % cW.weaponName, cW.unequipAnimSpeed, false)
|
||
await get_tree().create_timer(cW.unequipTime).timeout
|
||
|
||
cWModel.visible = false
|
||
|
||
enterWeapon(nextWeapon)
|
||
|
||
func enterWeapon(nextWeapon):
|
||
#this function manage the second part of the weapon switching mechanic
|
||
#in this part, the next weapon is enabled (equiped and set up)
|
||
|
||
# --- СВЕРХ-ЗАЩИТА ОТ БАГОВ СЛОВАРЯ (DICTIONARY) ПРИ СМЕНЕ УРОВНЯ ---
|
||
# Раз движок уверен, что weaponList — это Dictionary, проверяем наличие ключа через .has()
|
||
var is_valid_weapon = false
|
||
|
||
if weaponList != null and weaponList.has(nextWeapon):
|
||
is_valid_weapon = true
|
||
|
||
# Если оружие еще не готово или ключ '0' отсутствует в словаре на новом уровне:
|
||
if not is_valid_weapon:
|
||
print("[ОРУЖИЕ] Предупреждение: Ключ ", nextWeapon, " еще не инициализирован в словарe weaponList. Ждем...")
|
||
# Даем ассету один кадр очухаться и наполнить свои словари пушками
|
||
await get_tree().process_frame
|
||
|
||
# Повторная проверка
|
||
if weaponList != null and weaponList.has(nextWeapon):
|
||
is_valid_weapon = true
|
||
|
||
# Если всё еще пусто — аварийно выходим, чтобы спасти игру от вылета
|
||
if not is_valid_weapon:
|
||
print("[ОРУЖИЕ] КРИТИЧЕСКИЙ СБОЙ: Оружие полностью утеряно при генерации сцены!")
|
||
canUseWeapon = true
|
||
canChangeWeapons = true
|
||
return
|
||
|
||
cW = weaponList[nextWeapon]
|
||
|
||
# Проверяем наличие модели оружия перед включением видимости
|
||
if cW and cW.weaponSlot and "model" in cW.weaponSlot:
|
||
cWModel = cW.weaponSlot.model
|
||
if cWModel:
|
||
cWModel.visible = true
|
||
|
||
# Передаем обновленное оружие в менеджеры, если они существуют
|
||
if shootManager: shootManager.getCurrentWeapon(cW)
|
||
if reloadManager: reloadManager.getCurrentWeapon(cW)
|
||
if animManager: animManager.getCurrentWeapon(cW, cWModel)
|
||
|
||
if cW:
|
||
weaponSoundManagement(cW.equipSound, cW.equipSoundSpeed)
|
||
if animPlayer:
|
||
animPlayer.playback_default_blend_time = cW.animBlendTime
|
||
|
||
if cW.equipAnimName != "" and animManager:
|
||
animManager.playAnimation("EquipAnim%s" % cW.weaponName, cW.equipAnimSpeed, false)
|
||
|
||
await get_tree().create_timer(cW.equipTime).timeout
|
||
|
||
if cW.isShooting: cW.isShooting = false
|
||
if cW.isReloading: cW.isReloading = false
|
||
|
||
canUseWeapon = true
|
||
canChangeWeapons = true
|
||
|
||
|
||
|
||
func _process(_delta : float):
|
||
if cW != null and cWModel != null and canUseWeapon:
|
||
weaponInputs()
|
||
|
||
reloadManager.autoReload()
|
||
|
||
displayStats()
|
||
|
||
func weaponInputs():
|
||
if Input.is_action_pressed(shoot_action):
|
||
# --- ПЕРЕХВАТ ДЛЯ БОНУСА SPEEDBOOSTER ---
|
||
|
||
if cW != null and cW.weaponName == "SpeedBooster":
|
||
# Проверяем, есть ли заряды и что мы его уже не используем прямо сейчас
|
||
if cW.totalAmmoInMag > 0 and not cW.isShooting:
|
||
cW.isShooting = true
|
||
# === НАШЕ ДОБАВЛЕНИЕ: ПРИНУДИТЕЛЬНО ИГРАЕМ ЗВУК ИЗ РЕСУРСА ПРЕДМЕТА ===
|
||
# Берем звук, который ты прикрепил в поле shot_sound (или shootSound)
|
||
if "shotSound" in cW and cW.shotSound != null:
|
||
weaponSoundManagement(cW.shotSound, 1.0)
|
||
elif "shootSound" in cW and cW.shootSound != null:
|
||
weaponSoundManagement(cW.shootSound, 1.0)
|
||
# Тратим одно использование бонуса
|
||
cW.totalAmmoInMag -= 1
|
||
displayStats() # Обновляем цифры на HUD (станет 0)
|
||
|
||
print("[БОНУС] Успешный клик! Активирую скорость на PlayerCharacter.")
|
||
|
||
# Отправляем команду на ускорение в скрипт твоего игрока
|
||
if playChar and playChar.has_method("activate_speed_boost"):
|
||
playChar.activate_speed_boost()
|
||
|
||
# Ждем кулдаун использования банки (1 секунда, чтобы проигралась анимация глотка/использования)
|
||
await get_tree().create_timer(1.0).timeout
|
||
|
||
# --- УДАЛЕНИЕ ИЗ ИНВЕНТАРЯ ПОСЛЕ ИСПОЛЬЗОВАНИЯ ---
|
||
print("[БОНУС] Энергетик пуст. Удаляю его из инвентаря игрока...")
|
||
|
||
# Находим, под каким номером в инвентаре (weaponStack) лежит наш SpeedBooster
|
||
var booster_stack_index = weaponStack.find(cW.weaponId)
|
||
if booster_stack_index != -1:
|
||
# Полностью выкидываем его из списка оружия игрока
|
||
weaponStack.remove_at(booster_stack_index)
|
||
|
||
# Принудительно прячем 3D-модельку пустой банки в руках
|
||
if cWModel:
|
||
cWModel.visible = false
|
||
|
||
# Переключаем инвентарь на самое первое доступное оружие (индекс 0 - автомат или пистолет)
|
||
weaponIndex = 0
|
||
if weaponStack.size() > 0:
|
||
changeWeapon(weaponStack[0])
|
||
else:
|
||
# Если у игрока вообще больше нет оружия, сбрасываем текущее оружие в null
|
||
cW = null
|
||
cWModel = null
|
||
canUseWeapon = false
|
||
|
||
# Возвращаем флаг стрельбы в исходное состояние
|
||
if cW != null:
|
||
cW.isShooting = false
|
||
|
||
else:
|
||
# ОРИГИНАЛЬНАЯ СТРЕЛЬБА АССЕТА (Для автоматов и пистолетов)
|
||
shootManager.shoot()
|
||
|
||
if Input.is_action_just_pressed(reload_action): reloadManager.reload()
|
||
|
||
if Input.is_action_just_pressed(weapon_wheel_up_action):
|
||
if canChangeWeapons and !cW.isShooting and !cW.isReloading:
|
||
weaponIndex = min(weaponIndex + 1, weaponStack.size() - 1) #from first element of weapon stack to last element
|
||
changeWeapon(weaponStack[weaponIndex])
|
||
|
||
if Input.is_action_just_pressed(weapon_wheel_down_action):
|
||
if canChangeWeapons and !cW.isShooting and !cW.isReloading:
|
||
weaponIndex = max(weaponIndex - 1, 0) #from last element of weapon stack to first element
|
||
changeWeapon(weaponStack[weaponIndex])
|
||
|
||
|
||
func displayStats():
|
||
if hud == null or cW == null:
|
||
return
|
||
|
||
hud.displayWeaponStack(weaponStack.size())
|
||
hud.displayWeaponName(cW.weaponName)
|
||
hud.displayTotalAmmoInMag(cW.totalAmmoInMag, cW.nbProjShotsAtSameTime)
|
||
|
||
# --- БРОНЕБОЙНАЯ ЗАЩИТА ОТ ВЫЛЕТА ИЗ-ЗА ПАТРОНОВ ПРИ СМЕНЕ СЦЕНЫ ---
|
||
# Используем метод .get() вместо квадратных скобок [...].
|
||
# Он никогда не уронит игру, даже если словарь ammoDict полностью пустой!
|
||
var total_ammo : int = 0
|
||
if ammoManager != null and ammoManager.ammoDict != null:
|
||
total_ammo = ammoManager.ammoDict.get(cW.ammoType, 0)
|
||
|
||
hud.displayTotalAmmo(total_ammo, cW.nbProjShotsAtSameTime)
|
||
|
||
|
||
# --- БРОНЕБОЙНАЯ ЗАЩИТА ОТ ВЫЛЕТА ПРИ ПЕРЕХОДЕ НА НОВЫЙ УРОВЕНЬ ---
|
||
# Проверяем, существует ли ammoManager и есть ли тип патронов текущего оружия в словаре
|
||
if ammoManager != null and ammoManager.ammoDict != null and ammoManager.ammoDict.has(cW.ammoType):
|
||
hud.displayTotalAmmo(ammoManager.ammoDict[cW.ammoType], cW.nbProjShotsAtSameTime)
|
||
else:
|
||
# Если словарь еще не загрузился, временно пишем 0, чтобы игра не вылетала
|
||
hud.displayTotalAmmo(0, cW.nbProjShotsAtSameTime)
|
||
|
||
func changeWeapon(nextWeapon : int):
|
||
if canChangeWeapons and !cW.isShooting and !cW.isReloading:
|
||
exitWeapon(nextWeapon)
|
||
else:
|
||
push_error("Can't change weapon now")
|
||
return
|
||
|
||
func displayMuzzleFlash():
|
||
#create a muzzle flash instance, and display it at the indicated point
|
||
if cW.muzzleFlashRef != null:
|
||
var muzzleFlashInstance = cW.muzzleFlashRef.instantiate()
|
||
add_child(muzzleFlashInstance)
|
||
muzzleFlashInstance.global_position = cW.weaponSlot.muzzleFlashSpawner.global_position
|
||
muzzleFlashInstance.emitting = true
|
||
else:
|
||
push_error("%s doesn't have a muzzle flash reference" % cW.weaponName)
|
||
return
|
||
|
||
# НАЙДИ И ПОЛНОСТЬСТЬЮ ЗАМЕНИ ЭТУ ФУНКЦИЮ В WeaponManagerScript.gd:
|
||
|
||
# Сначала загружаем файл искр в самом верху скрипта (к остальным preload, если есть, или прямо перед функцией):
|
||
const SPARKS_SCENE = preload("res://addons/Weapons/bullet_sparks.tscn.tscn") # Укажи правильный путь к файлу из Шага 1!
|
||
|
||
func displayBulletHole(colliderPoint : Vector3, colliderNormal : Vector3):
|
||
# 1. Оригинальный код автора для спавна декали дырки от пули
|
||
if bulletDecal != null:
|
||
var bulletDecalInstance = bulletDecal.instantiate()
|
||
get_tree().get_root().add_child(bulletDecalInstance)
|
||
bulletDecalInstance.global_position = colliderPoint
|
||
bulletDecalInstance.look_at(colliderPoint - colliderNormal, Vector3.UP)
|
||
bulletDecalInstance.rotate_object_local(Vector3(1.0, 0.0, 0.0), 90)
|
||
|
||
# 2. НАШЕ НОВОЕ ДОБАВЛЕНИЕ: СПАВН ПАРТИКЛОВ ИСКР
|
||
if SPARKS_SCENE != null:
|
||
# Было: var sparks_instance = SPARKS_SCENE.instAPE()
|
||
# НАДО (СТРОГО ВОТ ТАК):
|
||
var sparks_instance = SPARKS_SCENE.instantiate()
|
||
|
||
get_tree().get_root().add_child(sparks_instance)
|
||
|
||
# Слегка смещаем искры назад от стены (на 0.05 метра вдоль нормали),
|
||
# чтобы они гарантированно не рендерились внутри текстуры стены!
|
||
sparks_instance.global_position = colliderPoint + (colliderNormal * 0.05)
|
||
|
||
if colliderNormal.length() > 0.01:
|
||
sparks_instance.look_at(sparks_instance.global_position + colliderNormal, Vector3.UP)
|
||
|
||
|
||
|
||
|
||
func weaponSoundManagement(soundName : AudioStream, soundSpeed : float):
|
||
var audioIns : AudioStreamPlayer3D = audioManager.instantiate()
|
||
get_tree().get_root().add_child.call_deferred(audioIns)
|
||
#makes sure the node is in the scene tree
|
||
await get_tree().process_frame
|
||
if audioIns.is_inside_tree():
|
||
audioIns.global_transform = cW.weaponSlot.attackPoint.global_transform
|
||
audioIns.bus = "Sfx"
|
||
audioIns.pitch_scale = soundSpeed
|
||
audioIns.stream = soundName
|
||
audioIns.play()
|
||
else:
|
||
print("The sound can't be played, AudioStreamPlayer3D instance is not in the scene tree")
|
||
|
||
func forceAttackPointTransformValues(attackPoint : Marker3D):
|
||
#reset the attack points rotation values, to ensure that the projectiles will be shot in the correct direction
|
||
if attackPoint.rotation != Vector3.ZERO: attackPoint.rotation = Vector3.ZERO
|
||
|
||
|
||
# Добавьте этот код в самый конец вашего скрипта WeaponManager
|
||
|
||
# Замените эти две функции в самом конце вашего WeaponManager
|
||
|
||
func disable_weapons_for_dialogue() -> void:
|
||
canUseWeapon = false
|
||
canChangeWeapons = false
|
||
|
||
# Принудительно сбрасываем флаги стрельбы
|
||
if cW != null:
|
||
cW.isShooting = false
|
||
cW.isReloading = false
|
||
|
||
# ВЫКЛЮЧАЕМ менеджеры стрельбы и перезарядки, чтобы они не видели мышь
|
||
if shootManager:
|
||
shootManager.process_mode = Node.PROCESS_MODE_DISABLED
|
||
if reloadManager:
|
||
reloadManager.process_mode = Node.PROCESS_MODE_DISABLED
|
||
|
||
print("[WeaponManager] Стрельба и перезарядка ПОЛНОСТЬСТЬЮ заморожены.")
|
||
|
||
func enable_weapons_after_dialogue() -> void:
|
||
canUseWeapon = true
|
||
canChangeWeapons = true
|
||
|
||
# ВКЛЮЧАЕМ менеджеры обратно в стандартный режим работы
|
||
if shootManager:
|
||
shootManager.process_mode = Node.PROCESS_MODE_INHERIT
|
||
if reloadManager:
|
||
reloadManager.process_mode = Node.PROCESS_MODE_INHERIT
|
||
|
||
print("[WeaponManager] Оружие полностью разблокировано.")
|