2025-08-26 11:39:47 +08:00
|
|
|
extends Area2D
|
|
|
|
|
class_name BulletBase
|
|
|
|
|
|
2026-04-30 06:39:56 +08:00
|
|
|
enum MotionType {
|
2026-05-08 15:43:55 +08:00
|
|
|
SWING, # 近战挥舞
|
2026-04-30 06:42:15 +08:00
|
|
|
PROJECTILE, # 射弹
|
|
|
|
|
MAGIC, # 魔法
|
|
|
|
|
SUMMON, # 召唤
|
|
|
|
|
SPRINT, # 冲撞
|
2026-04-30 06:45:48 +08:00
|
|
|
BREATH, # 吐息
|
2026-05-08 15:43:55 +08:00
|
|
|
STAB, # 近战戳刺
|
2026-05-09 19:43:44 +08:00
|
|
|
EXPLOSION, # 爆炸
|
2026-04-30 06:39:56 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-01 09:12:27 +08:00
|
|
|
signal destroied(becauseMap: bool)
|
|
|
|
|
|
2025-09-13 20:52:19 +08:00
|
|
|
@export var displayName: String = "未知子弹"
|
2025-08-29 10:50:22 +08:00
|
|
|
@export var speed: float = 10.0
|
2025-11-22 08:42:51 +08:00
|
|
|
@export var baseDamage: float = 10.0
|
2026-04-30 06:39:56 +08:00
|
|
|
@export var motionType: MotionType = MotionType.PROJECTILE
|
2025-11-22 08:42:51 +08:00
|
|
|
@export var damageMultipliers: Array[float] = [1.0]
|
|
|
|
|
@export var usingDamageMultiplier: int = 0
|
2025-08-29 10:50:22 +08:00
|
|
|
@export var penerate: float = 0.0
|
2025-09-21 16:17:49 +08:00
|
|
|
@export var penerateDamageReduction: float = 0.0
|
2025-08-26 12:21:09 +08:00
|
|
|
@export var lifeDistance: float = -1 # -1表示无限距离
|
|
|
|
|
@export var lifeTime: float = -1 # -1表示无限时间
|
2026-01-28 20:20:55 +08:00
|
|
|
@export var allowFriendlyDamage: bool = false # 是否无差别伤害(不区分敌我)
|
2025-08-27 08:58:14 +08:00
|
|
|
@export var canDamageSelf: bool = false # 是否可以伤害发射者
|
2025-08-27 15:38:30 +08:00
|
|
|
@export var autoSpawnAnimation: bool = false
|
|
|
|
|
@export var autoLoopAnimation: bool = false
|
2025-08-29 11:37:25 +08:00
|
|
|
@export var autoDestroyAnimation: bool = false
|
2025-08-29 18:34:58 +08:00
|
|
|
@export var autoDestroyOnHitMap: bool = true
|
2026-03-07 09:05:36 +08:00
|
|
|
@export var autoPlayTexture: bool = false
|
2025-08-28 12:34:41 +08:00
|
|
|
@export var freeAfterSpawn: bool = false
|
|
|
|
|
@export var knockback: float = 0 # 击退力,物理引擎单位
|
2025-08-28 21:57:04 +08:00
|
|
|
@export var recoil: float = 0 # 后坐力,物理引擎单位
|
2026-01-23 23:59:02 +08:00
|
|
|
@export var canDoDuplicate: bool = true # 是否可以分裂、折射
|
2025-08-26 11:39:47 +08:00
|
|
|
|
2025-08-27 14:55:34 +08:00
|
|
|
@onready var animator: AnimationPlayer = $"%animator"
|
|
|
|
|
@onready var hitbox: CollisionShape2D = $"%hitbox"
|
|
|
|
|
@onready var texture: AnimatedSprite2D = $"%texture"
|
2025-08-27 13:30:50 +08:00
|
|
|
|
2025-08-26 11:39:47 +08:00
|
|
|
var launcher: EntityBase = null
|
2025-11-08 20:19:24 +08:00
|
|
|
var launcherSummoned: EntityBase = null
|
2025-12-13 07:55:02 +08:00
|
|
|
var parent: BulletBase = null
|
2025-08-26 12:21:09 +08:00
|
|
|
var spawnInWhen: float = 0
|
|
|
|
|
var spawnInWhere: Vector2 = Vector2.ZERO
|
2025-08-29 12:42:44 +08:00
|
|
|
var destroying: bool = false
|
2026-01-18 14:12:34 +08:00
|
|
|
var canDuplicateSelf: bool = true
|
2025-09-13 19:55:51 +08:00
|
|
|
var initialSpeed: float = 0
|
2025-11-22 09:11:40 +08:00
|
|
|
var initialDamage: float = 0
|
2025-11-23 06:51:48 +08:00
|
|
|
var speedScale: float = 1
|
2025-12-14 17:01:09 +08:00
|
|
|
var isFirstFrame: bool = true
|
2026-03-22 08:16:35 +08:00
|
|
|
var cycleStateAngle: float = 0
|
2026-04-18 08:12:27 +08:00
|
|
|
var lastDelta: float = 0
|
2025-08-26 11:39:47 +08:00
|
|
|
|
|
|
|
|
func _ready():
|
2025-09-13 19:55:51 +08:00
|
|
|
initialSpeed = speed
|
2025-11-22 09:11:40 +08:00
|
|
|
initialDamage = baseDamage
|
2025-11-08 20:19:24 +08:00
|
|
|
if launcher.isSummon():
|
|
|
|
|
launcherSummoned = launcher
|
|
|
|
|
launcher = launcher.myMaster
|
2025-08-29 10:50:22 +08:00
|
|
|
register()
|
2026-01-27 20:52:26 +08:00
|
|
|
area_entered.connect(hitEntity)
|
|
|
|
|
body_entered.connect(hitObstacle)
|
2025-08-29 10:27:32 +08:00
|
|
|
spawnInWhen = WorldManager.getTime()
|
2025-08-26 12:21:09 +08:00
|
|
|
spawnInWhere = position
|
2025-08-27 13:30:50 +08:00
|
|
|
spawn()
|
2025-08-28 21:14:36 +08:00
|
|
|
dotLoop()
|
2025-08-27 15:38:30 +08:00
|
|
|
if autoLoopAnimation:
|
|
|
|
|
animator.play("loop")
|
2026-03-07 09:05:36 +08:00
|
|
|
if autoPlayTexture:
|
|
|
|
|
texture.play("default")
|
2025-08-29 18:34:58 +08:00
|
|
|
body_entered.connect(
|
|
|
|
|
func(body):
|
|
|
|
|
if body.is_in_group("map"):
|
|
|
|
|
if autoDestroyOnHitMap:
|
|
|
|
|
tryDestroy(true)
|
|
|
|
|
)
|
2026-03-16 23:35:22 +08:00
|
|
|
area_entered.connect(
|
|
|
|
|
func(body):
|
|
|
|
|
var bullet = BulletTool.fromArea(body)
|
|
|
|
|
if is_instance_valid(bullet):
|
|
|
|
|
hitBullet(bullet)
|
|
|
|
|
)
|
2025-09-21 08:26:29 +08:00
|
|
|
ai()
|
2026-03-16 23:35:22 +08:00
|
|
|
if autoSpawnAnimation:
|
|
|
|
|
animator.play("spawn")
|
|
|
|
|
await animator.animation_finished
|
2026-03-18 22:39:44 +08:00
|
|
|
afterSpawn()
|
2026-03-16 23:35:22 +08:00
|
|
|
if freeAfterSpawn:
|
|
|
|
|
tryDestroy()
|
2025-08-26 12:21:09 +08:00
|
|
|
func _process(_delta: float) -> void:
|
2025-08-29 12:42:44 +08:00
|
|
|
if destroying: return
|
2025-08-26 12:21:09 +08:00
|
|
|
if lifeTime > 0:
|
2025-08-29 10:27:32 +08:00
|
|
|
if WorldManager.getTime() - spawnInWhen >= lifeTime:
|
2025-08-29 12:42:44 +08:00
|
|
|
tryDestroy()
|
2025-08-26 12:21:09 +08:00
|
|
|
if lifeDistance > 0:
|
|
|
|
|
if position.distance_to(spawnInWhere) >= lifeDistance:
|
2025-08-29 12:42:44 +08:00
|
|
|
tryDestroy()
|
2026-04-18 08:12:27 +08:00
|
|
|
func _physics_process(delta: float) -> void:
|
|
|
|
|
lastDelta = delta
|
2025-08-29 12:42:44 +08:00
|
|
|
if destroying: return
|
2025-08-27 08:09:47 +08:00
|
|
|
if is_instance_valid(launcher) and (launcher.isPlayer() or is_instance_valid(launcher.currentFocusedBoss)):
|
2025-08-28 21:57:04 +08:00
|
|
|
launcher.position -= Vector2.from_angle(rotation) * recoil
|
2025-09-06 14:11:14 +08:00
|
|
|
var targetEntity = EntityTool.findClosetEntity(position, get_tree(),
|
|
|
|
|
!launcher.isPlayer(),
|
|
|
|
|
launcher.isPlayer(),
|
2025-09-06 18:33:11 +08:00
|
|
|
[launcher])
|
2025-09-06 14:11:14 +08:00
|
|
|
if is_instance_valid(targetEntity):
|
2025-09-06 18:33:11 +08:00
|
|
|
PresetBulletAI.trace(
|
2026-03-07 09:05:36 +08:00
|
|
|
self ,
|
2025-09-06 14:11:14 +08:00
|
|
|
targetEntity.getTrackingAnchor(),
|
|
|
|
|
launcher.fields.get(FieldStore.Entity.BULLET_TRACE) / 10
|
|
|
|
|
)
|
2025-12-14 17:01:09 +08:00
|
|
|
if isFirstFrame:
|
|
|
|
|
firstFrame()
|
|
|
|
|
isFirstFrame = false
|
2025-08-27 08:09:47 +08:00
|
|
|
ai()
|
2025-09-05 22:23:41 +08:00
|
|
|
else:
|
|
|
|
|
tryDestroy()
|
2025-08-26 11:39:47 +08:00
|
|
|
|
2026-04-03 18:29:14 +08:00
|
|
|
func findAnchor(anch: String):
|
|
|
|
|
var node = get_node_or_null("%anchor" + anch)
|
|
|
|
|
if node is Node2D:
|
|
|
|
|
return node.global_position
|
|
|
|
|
else:
|
|
|
|
|
return Vector2.ZERO
|
2025-11-23 06:51:48 +08:00
|
|
|
func setupCuttable(cutSpeed: float):
|
2025-11-23 07:10:49 +08:00
|
|
|
area_entered.connect(
|
2025-11-23 06:51:48 +08:00
|
|
|
func(body):
|
|
|
|
|
var entity = EntityTool.fromHurtbox(body)
|
|
|
|
|
if entity:
|
|
|
|
|
speedScale = cutSpeed
|
|
|
|
|
)
|
2025-11-23 07:10:49 +08:00
|
|
|
area_exited.connect(
|
2025-11-23 06:51:48 +08:00
|
|
|
func(body):
|
|
|
|
|
var entity = EntityTool.fromHurtbox(body)
|
|
|
|
|
if entity:
|
|
|
|
|
speedScale = 1
|
|
|
|
|
)
|
2025-11-22 08:25:26 +08:00
|
|
|
func getDamage():
|
2025-11-29 21:39:56 +08:00
|
|
|
return baseDamage * damageMultipliers[usingDamageMultiplier]
|
2026-05-10 14:58:05 +08:00
|
|
|
func calculateDamage(crit: bool, entity: Variant):
|
2026-01-27 20:52:26 +08:00
|
|
|
var baseDmg = getDamage() * launcher.fields.get(FieldStore.Entity.DAMAGE_MULTIPILER) * randf_range(1 - GameRule.damageOffset, 1 + GameRule.damageOffset)
|
|
|
|
|
var damage = baseDmg + baseDmg * int(crit) * launcher.fields.get(FieldStore.Entity.CRIT_DAMAGE)
|
2026-05-10 14:58:05 +08:00
|
|
|
return damageOverride(damage, entity)
|
2026-01-27 20:52:26 +08:00
|
|
|
func determineCrit():
|
|
|
|
|
return MathTool.rate(launcher.fields.get(FieldStore.Entity.CRIT_RATE) + GameRule.critRateInfluenceByLuckValue * launcher.fields[FieldStore.Entity.LUCK_VALUE])
|
|
|
|
|
func hitEntity(target: Node):
|
2025-08-26 11:39:47 +08:00
|
|
|
var entity: EntityBase = EntityTool.fromHurtbox(target)
|
2026-03-07 09:05:36 +08:00
|
|
|
if !BulletTool.canDamage(self , entity): return
|
|
|
|
|
var resultDamage = entity.bulletHit(self , determineCrit())
|
2025-09-14 22:38:08 +08:00
|
|
|
succeedToHit(resultDamage, entity)
|
2026-01-27 20:52:26 +08:00
|
|
|
if MathTool.rate(fullPenerate() - entity.fields[FieldStore.Entity.PENARATION_RESISTANCE]):
|
2025-11-22 08:42:51 +08:00
|
|
|
baseDamage *= 1.0 - penerateDamageReduction
|
2025-08-27 12:46:20 +08:00
|
|
|
else:
|
2025-08-29 12:42:44 +08:00
|
|
|
tryDestroy()
|
2026-01-27 20:52:26 +08:00
|
|
|
func hitObstacle(target: Node):
|
|
|
|
|
if target is ObstacleBase:
|
|
|
|
|
var obstacle = target as ObstacleBase
|
|
|
|
|
if is_instance_valid(obstacle.launcher):
|
2026-03-07 09:05:36 +08:00
|
|
|
if not BulletTool.canDamage(self , obstacle.launcher): return
|
2026-05-10 14:58:05 +08:00
|
|
|
obstacle.takeDamage(calculateDamage(determineCrit(), target))
|
2026-01-27 20:52:26 +08:00
|
|
|
if MathTool.rate(fullPenerate() - obstacle.penerateResistance):
|
|
|
|
|
baseDamage *= 1.0 - penerateDamageReduction
|
|
|
|
|
else:
|
|
|
|
|
tryDestroy()
|
2025-08-26 12:21:09 +08:00
|
|
|
func forward(direction: Vector2):
|
2025-08-29 10:50:22 +08:00
|
|
|
position += direction.normalized() * speed * GameRule.bulletSpeedMultiplier
|
2025-08-27 12:46:20 +08:00
|
|
|
func fullPenerate():
|
2025-08-29 10:50:22 +08:00
|
|
|
return penerate + launcher.fields.get(FieldStore.Entity.PENERATE) + GameRule.penerateRateInfluenceByLuckValue * launcher.fields[FieldStore.Entity.LUCK_VALUE]
|
2025-08-27 14:55:34 +08:00
|
|
|
func timeLived():
|
2025-08-29 10:27:32 +08:00
|
|
|
return WorldManager.getTime() - spawnInWhen
|
2025-11-16 16:01:03 +08:00
|
|
|
func distanceLived():
|
|
|
|
|
return position.distance_to(spawnInWhere)
|
2025-12-14 17:01:09 +08:00
|
|
|
func lifeTimePercent():
|
|
|
|
|
return timeLived() / lifeTime
|
2025-11-16 16:01:03 +08:00
|
|
|
func lifeDistancePercent():
|
|
|
|
|
return distanceLived() / lifeDistance
|
2025-08-27 19:59:05 +08:00
|
|
|
func dotLoop():
|
|
|
|
|
if await applyDot():
|
2025-09-25 21:57:32 +08:00
|
|
|
await TickTool.until(func(): return !UIState.currentPanel)
|
2025-08-27 19:59:05 +08:00
|
|
|
await dotLoop()
|
2025-08-29 18:34:58 +08:00
|
|
|
func tryDestroy(becauseMap: bool = false):
|
2025-08-29 12:42:44 +08:00
|
|
|
if destroying: return
|
|
|
|
|
destroying = true
|
2026-01-23 23:59:02 +08:00
|
|
|
if canDoDuplicate:
|
|
|
|
|
trySplit()
|
|
|
|
|
tryRefract()
|
2025-08-29 18:34:58 +08:00
|
|
|
await destroy(becauseMap)
|
2025-08-29 12:42:44 +08:00
|
|
|
if autoDestroyAnimation:
|
|
|
|
|
animator.play("destroy")
|
|
|
|
|
await animator.animation_finished
|
2026-05-01 09:12:27 +08:00
|
|
|
destroied.emit(becauseMap)
|
2025-08-29 12:42:44 +08:00
|
|
|
queue_free()
|
2025-09-05 22:23:41 +08:00
|
|
|
func trySplit():
|
2026-01-18 14:12:34 +08:00
|
|
|
if is_instance_valid(launcher) and canDuplicateSelf:
|
2025-12-14 15:36:19 +08:00
|
|
|
var value = launcher.fields.get(FieldStore.Entity.BULLET_SPLIT)
|
|
|
|
|
var total = MathTool.shrimpRate(value)
|
2025-12-14 15:37:26 +08:00
|
|
|
var last = value - floor(value)
|
2025-12-14 15:36:19 +08:00
|
|
|
for i in total:
|
2026-01-17 12:21:38 +08:00
|
|
|
var cloned = duplicate() as BulletBase
|
2026-01-18 14:12:34 +08:00
|
|
|
cloned.rotation += deg_to_rad(360.0 / total * i + 180)
|
|
|
|
|
cloned.canDuplicateSelf = false
|
2026-01-17 13:22:10 +08:00
|
|
|
cloned.launcher = launcher
|
2026-01-18 14:51:54 +08:00
|
|
|
if is_instance_valid(parent):
|
|
|
|
|
cloned.parent = parent
|
2026-03-22 08:16:35 +08:00
|
|
|
var splited = split(cloned, i, total, last)
|
|
|
|
|
if !is_instance_valid(splited): continue
|
|
|
|
|
get_parent().add_child.call_deferred(splited)
|
2025-09-05 22:23:41 +08:00
|
|
|
func tryRefract():
|
2026-01-18 14:12:34 +08:00
|
|
|
if is_instance_valid(launcher) and canDuplicateSelf:
|
2025-09-05 22:23:41 +08:00
|
|
|
var value = launcher.fields.get(FieldStore.Entity.BULLET_REFRACTION)
|
2025-12-14 15:36:19 +08:00
|
|
|
var total = MathTool.shrimpRate(value)
|
2025-12-14 15:37:26 +08:00
|
|
|
var last = value - floor(value)
|
2026-01-17 13:22:10 +08:00
|
|
|
var aimed: Array[EntityBase] = []
|
2025-12-14 15:36:19 +08:00
|
|
|
for i in total:
|
2026-01-17 13:22:10 +08:00
|
|
|
var entity = EntityTool.findClosetEntity(
|
|
|
|
|
position, get_tree(),
|
|
|
|
|
!launcher.isPlayer(),
|
|
|
|
|
launcher.isPlayer(),
|
|
|
|
|
[launcher] + aimed
|
|
|
|
|
)
|
2025-09-06 14:11:14 +08:00
|
|
|
if is_instance_valid(entity):
|
2026-01-17 13:22:10 +08:00
|
|
|
aimed.append(entity)
|
2026-01-17 12:21:38 +08:00
|
|
|
var cloned = duplicate() as BulletBase
|
|
|
|
|
cloned.look_at(entity.position)
|
2026-01-18 14:12:34 +08:00
|
|
|
cloned.canDuplicateSelf = false
|
2026-01-17 13:22:10 +08:00
|
|
|
cloned.launcher = launcher
|
2026-01-18 14:51:54 +08:00
|
|
|
if is_instance_valid(parent):
|
|
|
|
|
cloned.parent = parent
|
2026-03-22 08:16:35 +08:00
|
|
|
var refracted = refract(cloned, entity, i, total, last)
|
|
|
|
|
if !is_instance_valid(refracted): continue
|
|
|
|
|
get_parent().add_child.call_deferred(refracted)
|
2025-08-26 12:21:09 +08:00
|
|
|
|
2025-08-29 12:17:40 +08:00
|
|
|
# 抽象方法
|
2025-12-14 17:01:09 +08:00
|
|
|
func firstFrame():
|
|
|
|
|
pass
|
2025-08-26 12:21:09 +08:00
|
|
|
func ai():
|
|
|
|
|
pass
|
2025-08-29 18:34:58 +08:00
|
|
|
func destroy(_beacuseMap: bool):
|
2025-08-29 12:42:44 +08:00
|
|
|
pass
|
2025-08-27 13:30:50 +08:00
|
|
|
func spawn():
|
|
|
|
|
pass
|
2026-03-18 22:39:44 +08:00
|
|
|
func afterSpawn():
|
|
|
|
|
pass
|
2025-08-27 19:59:05 +08:00
|
|
|
func applyDot():
|
|
|
|
|
pass
|
2025-09-14 22:38:08 +08:00
|
|
|
func succeedToHit(_dmg: float, _entity: EntityBase):
|
2025-08-28 15:53:39 +08:00
|
|
|
pass
|
2025-08-29 10:50:22 +08:00
|
|
|
func register():
|
|
|
|
|
pass
|
2026-01-17 12:40:58 +08:00
|
|
|
func split(newBullet: BulletBase, _index: int, _total: int, _lastBullet: float):
|
|
|
|
|
return newBullet
|
|
|
|
|
func refract(newBullet: BulletBase, _entity: EntityBase, _index: int, _total: int, _lastBullet: float):
|
|
|
|
|
return newBullet
|
2026-03-16 23:35:22 +08:00
|
|
|
func hitBullet(_bullet: BulletBase):
|
|
|
|
|
pass
|
2026-05-10 14:58:05 +08:00
|
|
|
func damageOverride(origin: float, _something: Variant):
|
|
|
|
|
return origin
|
2025-08-26 11:39:47 +08:00
|
|
|
|
|
|
|
|
static func generate(
|
|
|
|
|
bullet: PackedScene,
|
|
|
|
|
launchBy: EntityBase,
|
|
|
|
|
spawnPosition: Vector2,
|
|
|
|
|
spawnRotation: float,
|
2025-11-15 20:18:04 +08:00
|
|
|
addToWorld: bool = true,
|
|
|
|
|
ignoreOffset: bool = false
|
2025-08-26 11:39:47 +08:00
|
|
|
):
|
2025-08-27 11:08:11 +08:00
|
|
|
var extraCount = launchBy.fields.get(FieldStore.Entity.EXTRA_BULLET_COUNT)
|
2025-09-05 22:23:41 +08:00
|
|
|
var count = 1 + MathTool.shrimpRate(extraCount)
|
2025-08-27 11:08:11 +08:00
|
|
|
var instances = []
|
|
|
|
|
for i in range(count):
|
|
|
|
|
var instance: BulletBase = bullet.instantiate()
|
2025-09-06 08:50:37 +08:00
|
|
|
instance.launcher = launchBy
|
|
|
|
|
instance.position = spawnPosition
|
2025-11-15 20:18:04 +08:00
|
|
|
instance.rotation = spawnRotation + deg_to_rad(launchBy.fields.get(FieldStore.Entity.OFFSET_SHOOT) * randf_range(-1, 1) * int(!ignoreOffset))
|
2025-09-06 08:50:37 +08:00
|
|
|
if addToWorld:
|
2026-05-10 14:58:31 +08:00
|
|
|
WorldManager.rootNode.add_child.call_deferred(instance)
|
2025-09-08 22:35:24 +08:00
|
|
|
instance.add_to_group("bullets")
|
2025-09-06 08:50:37 +08:00
|
|
|
instances.append(instance)
|
2025-09-06 09:01:46 +08:00
|
|
|
return instances
|