2025-09-05 22:23:41 +08:00
|
|
|
|
@tool
|
|
|
|
|
|
extends PanelContainer
|
|
|
|
|
|
class_name Weapon
|
|
|
|
|
|
|
2026-04-13 23:07:48 +08:00
|
|
|
|
enum EmitType {
|
|
|
|
|
|
HOLD_SHOOT,
|
|
|
|
|
|
CLICK_SHOOT,
|
2026-04-13 23:08:46 +08:00
|
|
|
|
CHARGE,
|
2026-04-18 08:12:27 +08:00
|
|
|
|
HOLD_LOOP
|
2026-04-13 23:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 13:34:51 +08:00
|
|
|
|
@export var avatarTexture: Texture2D = null
|
2025-09-30 17:51:22 +08:00
|
|
|
|
@export var displayName: String = "未命名武器"
|
2025-09-05 22:23:41 +08:00
|
|
|
|
@export var quality: WeaponName.Quality = WeaponName.Quality.COMMON
|
|
|
|
|
|
@export var typeTopic: WeaponName.TypeTopic = WeaponName.TypeTopic.IMPACT
|
2025-09-20 07:01:17 +08:00
|
|
|
|
@export var soulLevel: int = 1
|
2025-09-20 17:23:57 +08:00
|
|
|
|
@export var costBeachball: int = 500
|
2026-04-13 23:07:48 +08:00
|
|
|
|
@export var emitType: EmitType = EmitType.HOLD_SHOOT
|
2025-09-05 22:23:41 +08:00
|
|
|
|
@export var store: Dictionary = {
|
|
|
|
|
|
"atk": 10
|
|
|
|
|
|
}
|
2025-09-06 16:11:59 +08:00
|
|
|
|
@export var storeType: Dictionary = {
|
2025-09-17 22:25:27 +08:00
|
|
|
|
"atk": FieldStore.DataType.INTEGER
|
2025-09-06 16:11:59 +08:00
|
|
|
|
}
|
2026-01-23 22:31:23 +08:00
|
|
|
|
@export_multiline var descriptionTemplate: String = "造成$atk点伤害。"
|
2026-04-04 08:55:15 +08:00
|
|
|
|
@export var sources: Array[String] = []
|
2026-04-04 09:11:45 +08:00
|
|
|
|
@export var tease: String = ""
|
2025-09-05 22:23:41 +08:00
|
|
|
|
@export var needEnergy: float = 0
|
|
|
|
|
|
@export var cooldown: float = 100
|
2025-09-06 08:17:10 +08:00
|
|
|
|
@export var debugRebuild: bool = false
|
2025-09-06 11:14:02 +08:00
|
|
|
|
@export var level: int = 0
|
2025-09-05 22:23:41 +08:00
|
|
|
|
|
2026-05-03 17:45:00 +08:00
|
|
|
|
@onready var avatarRect: TextureRect = $%avatar
|
|
|
|
|
|
@onready var nameLabel: WeaponName = $%name
|
|
|
|
|
|
@onready var sourceLabel: Label = $%source
|
|
|
|
|
|
@onready var teaseLabel: Label = $%tease
|
|
|
|
|
|
@onready var energyLabel: Label = $%energy
|
|
|
|
|
|
@onready var beachball: ItemShow = $%beachball
|
|
|
|
|
|
@onready var soul: ItemShow = $%soul
|
|
|
|
|
|
@onready var descriptionLabel: RichTextLabel = $%description
|
|
|
|
|
|
@onready var sounds: Node2D = $%sounds
|
|
|
|
|
|
@onready var moveLeftBtn: Button = $%moveleft
|
|
|
|
|
|
@onready var moveRightBtn: Button = $%moveright
|
|
|
|
|
|
|
|
|
|
|
|
@onready var autoUpdateBtn: Button = $%autoUpdateBtn
|
|
|
|
|
|
@onready var onceUpdateBtn: Button = $%onceUpdateBtn
|
|
|
|
|
|
@onready var updateBtn: Button = $%updateBtn
|
|
|
|
|
|
@onready var extractBtn: Button = $%extractBtn
|
|
|
|
|
|
@onready var inlayBtn: Button = $%inlayBtn
|
2025-09-05 22:23:41 +08:00
|
|
|
|
|
2026-05-10 11:49:17 +08:00
|
|
|
|
@onready var sublimateOptionsBox: Control = $%sublimateOptions
|
|
|
|
|
|
|
2025-09-06 15:24:50 +08:00
|
|
|
|
var cooldownTimer: CooldownTimer = null
|
2025-09-06 11:14:02 +08:00
|
|
|
|
var originalStore: Dictionary = {}
|
2026-02-05 20:04:39 +08:00
|
|
|
|
var chargedTime: float = 0
|
2026-03-07 09:05:36 +08:00
|
|
|
|
var attackSpeed: float = 1
|
2026-04-18 08:12:27 +08:00
|
|
|
|
var looping: bool = false
|
2026-05-03 17:45:00 +08:00
|
|
|
|
var autoUpdate: bool = false
|
2026-05-10 10:01:03 +08:00
|
|
|
|
var storeExtra: Dictionary = {}
|
2026-05-10 11:49:17 +08:00
|
|
|
|
var sublimateOptionsStored: Array[SublimateOption] = []
|
2025-09-06 07:40:21 +08:00
|
|
|
|
|
2025-09-05 22:23:41 +08:00
|
|
|
|
func _ready():
|
2025-09-06 15:24:50 +08:00
|
|
|
|
cooldownTimer = CooldownTimer.new()
|
|
|
|
|
|
cooldownTimer.cooldown = cooldown
|
2025-09-06 11:14:02 +08:00
|
|
|
|
originalStore = store
|
2026-05-03 17:45:00 +08:00
|
|
|
|
autoUpdateBtn.toggled.connect(
|
|
|
|
|
|
func(on: bool):
|
|
|
|
|
|
autoUpdate = on
|
|
|
|
|
|
)
|
|
|
|
|
|
onceUpdateBtn.pressed.connect(
|
|
|
|
|
|
func():
|
|
|
|
|
|
var count = 0
|
|
|
|
|
|
while canUpdate(UIState.player):
|
|
|
|
|
|
updateApply(UIState.player)
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
if count > 0:
|
|
|
|
|
|
UIState.showTip("一键强化提升了[b]%d[/b]级!" % count)
|
|
|
|
|
|
else:
|
|
|
|
|
|
UIState.showTip("一键强化没有提升等级......", TipBox.MessageType.ERROR)
|
|
|
|
|
|
)
|
2025-09-20 07:01:17 +08:00
|
|
|
|
updateBtn.pressed.connect(
|
2025-09-05 22:23:41 +08:00
|
|
|
|
func():
|
2026-05-03 17:45:00 +08:00
|
|
|
|
updateApply(UIState.player)
|
2025-09-05 22:23:41 +08:00
|
|
|
|
)
|
2025-09-20 07:01:17 +08:00
|
|
|
|
extractBtn.pressed.connect(
|
|
|
|
|
|
func():
|
2025-09-20 07:03:55 +08:00
|
|
|
|
if soulLevel > WeaponName.SoulLevel.NORMALIZE:
|
|
|
|
|
|
UIState.player.getItem({
|
2025-09-21 22:20:53 +08:00
|
|
|
|
ItemStore.ItemType.SOUL: soulLevel - 1
|
2025-09-20 07:03:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
soulLevel -= 1
|
2025-09-20 07:08:18 +08:00
|
|
|
|
updateStore(level, UIState.player)
|
2025-09-20 07:03:55 +08:00
|
|
|
|
rebuildInfo()
|
2026-05-04 18:14:21 +08:00
|
|
|
|
else:
|
|
|
|
|
|
UIState.showTip("[b]%s[/b]还未镶嵌任何灵魂!" % displayName, TipBox.MessageType.ERROR)
|
2025-09-20 07:01:17 +08:00
|
|
|
|
)
|
|
|
|
|
|
inlayBtn.pressed.connect(
|
|
|
|
|
|
func():
|
2025-09-20 07:08:18 +08:00
|
|
|
|
if soulLevel < WeaponName.SoulLevel.INFINITY:
|
2025-09-20 07:01:17 +08:00
|
|
|
|
if UIState.player.useItem({
|
|
|
|
|
|
ItemStore.ItemType.SOUL: soulLevel
|
|
|
|
|
|
}):
|
|
|
|
|
|
soulLevel += 1
|
2025-09-20 07:08:18 +08:00
|
|
|
|
updateStore(level, UIState.player)
|
2025-09-20 07:01:17 +08:00
|
|
|
|
rebuildInfo()
|
2026-05-04 18:14:21 +08:00
|
|
|
|
else:
|
|
|
|
|
|
UIState.showTip("持有的灵魂数量不足!", TipBox.MessageType.ERROR)
|
|
|
|
|
|
else:
|
|
|
|
|
|
UIState.showTip("[b]%s[/b]的灵魂槽位已满!" % displayName, TipBox.MessageType.ERROR)
|
2025-09-20 07:01:17 +08:00
|
|
|
|
)
|
2025-09-21 21:58:57 +08:00
|
|
|
|
moveLeftBtn.pressed.connect(
|
|
|
|
|
|
func():
|
2025-09-21 22:20:53 +08:00
|
|
|
|
if get_parent():
|
|
|
|
|
|
var myIndex = get_index()
|
|
|
|
|
|
var leftIndex = max(myIndex - 1, 0)
|
2026-03-07 09:05:36 +08:00
|
|
|
|
get_parent().move_child(self , leftIndex)
|
2025-09-21 22:20:53 +08:00
|
|
|
|
ArrayTool.swap(UIState.player.weapons, myIndex, leftIndex)
|
|
|
|
|
|
UIState.player.rebuildWeaponIcons()
|
2025-09-21 21:58:57 +08:00
|
|
|
|
)
|
|
|
|
|
|
moveRightBtn.pressed.connect(
|
|
|
|
|
|
func():
|
2025-09-21 22:20:53 +08:00
|
|
|
|
if get_parent():
|
|
|
|
|
|
var myIndex = get_index()
|
|
|
|
|
|
var rightIndex = min(myIndex + 1, get_parent().get_child_count() - 1)
|
2026-03-07 09:05:36 +08:00
|
|
|
|
get_parent().move_child(self , rightIndex)
|
2025-09-21 22:20:53 +08:00
|
|
|
|
ArrayTool.swap(UIState.player.weapons, myIndex, rightIndex)
|
|
|
|
|
|
UIState.player.rebuildWeaponIcons()
|
2025-09-21 21:58:57 +08:00
|
|
|
|
)
|
2025-09-06 08:50:37 +08:00
|
|
|
|
for i in sounds.get_children():
|
|
|
|
|
|
i.process_mode = ProcessMode.PROCESS_MODE_ALWAYS
|
2025-09-06 08:17:10 +08:00
|
|
|
|
debugRebuild = false # 只能在编辑器里打开
|
2026-05-10 11:49:17 +08:00
|
|
|
|
rebuildInfo()
|
2025-09-06 08:17:10 +08:00
|
|
|
|
func _physics_process(_delta):
|
|
|
|
|
|
if debugRebuild:
|
|
|
|
|
|
rebuildInfo()
|
2025-09-05 22:23:41 +08:00
|
|
|
|
|
2026-05-03 17:45:00 +08:00
|
|
|
|
func canUpdate(entity: EntityBase):
|
|
|
|
|
|
return entity.hasItem({ItemStore.ItemType.BEACHBALL: costBeachball})
|
2025-10-01 07:58:09 +08:00
|
|
|
|
func canInlay():
|
|
|
|
|
|
return UIState.player.hasItem({ItemStore.ItemType.SOUL: soulLevel})
|
2026-05-03 17:45:00 +08:00
|
|
|
|
func updateApply(entity: EntityBase) -> bool:
|
|
|
|
|
|
if canUpdate(entity):
|
2025-09-06 11:59:24 +08:00
|
|
|
|
level += 1
|
2025-09-06 11:14:02 +08:00
|
|
|
|
entity.inventory[ItemStore.ItemType.BEACHBALL] -= costBeachball
|
2025-09-20 07:08:18 +08:00
|
|
|
|
updateStore(level, entity)
|
2025-09-09 22:32:07 +08:00
|
|
|
|
costBeachball = floor(GameRule.weaponUpdateCost * costBeachball)
|
2025-10-01 07:58:09 +08:00
|
|
|
|
rebuildInfo(true)
|
|
|
|
|
|
return true
|
2026-05-03 17:45:00 +08:00
|
|
|
|
else:
|
|
|
|
|
|
UIState.showTip("沙滩球不足!", TipBox.MessageType.ERROR)
|
2025-10-01 07:58:09 +08:00
|
|
|
|
return false
|
2025-09-20 07:08:18 +08:00
|
|
|
|
func updateStore(to: int, entity: EntityBase):
|
|
|
|
|
|
store = update(to, originalStore.duplicate(), entity)
|
2025-09-05 22:23:41 +08:00
|
|
|
|
func multipiler() -> float:
|
|
|
|
|
|
if is_instance_valid(UIState.player):
|
|
|
|
|
|
return 1 - UIState.player.fields.get(FieldStore.Entity.PRICE_REDUCTION)
|
|
|
|
|
|
else:
|
|
|
|
|
|
return 1
|
2025-10-01 07:58:09 +08:00
|
|
|
|
func rebuildInfo(showNext: bool = false):
|
2026-05-10 11:49:17 +08:00
|
|
|
|
rebuildBaseInfo()
|
|
|
|
|
|
rebuildDescription(showNext)
|
|
|
|
|
|
rebuildSublimateOptions(showNext)
|
|
|
|
|
|
func rebuildBaseInfo():
|
2025-09-05 22:23:41 +08:00
|
|
|
|
avatarRect.texture = avatarTexture
|
|
|
|
|
|
nameLabel.displayName = displayName
|
|
|
|
|
|
nameLabel.quality = quality
|
|
|
|
|
|
nameLabel.typeTopic = typeTopic
|
2025-09-19 22:21:32 +08:00
|
|
|
|
nameLabel.soulLevel = soulLevel
|
2025-09-06 11:59:24 +08:00
|
|
|
|
nameLabel.level = level
|
2026-04-04 08:55:15 +08:00
|
|
|
|
sourceLabel.text = " × ".join(sources)
|
2026-04-04 13:32:15 +08:00
|
|
|
|
if len(tease) > 0:
|
|
|
|
|
|
teaseLabel.text = "“%s”" % tease
|
|
|
|
|
|
teaseLabel.show()
|
|
|
|
|
|
else:
|
|
|
|
|
|
teaseLabel.hide()
|
2025-09-05 22:23:41 +08:00
|
|
|
|
energyLabel.text = "%.1f" % needEnergy
|
2025-10-01 07:58:09 +08:00
|
|
|
|
beachball.count = costBeachball
|
|
|
|
|
|
soul.count = soulLevel
|
|
|
|
|
|
if is_instance_valid(UIState.player):
|
2026-05-03 17:45:00 +08:00
|
|
|
|
beachball.enough = canUpdate(UIState.player)
|
2025-10-01 07:58:09 +08:00
|
|
|
|
soul.enough = canInlay()
|
2025-09-20 17:23:30 +08:00
|
|
|
|
func formatValue(value: Variant, type: FieldStore.DataType) -> String:
|
|
|
|
|
|
if type == FieldStore.DataType.VALUE:
|
|
|
|
|
|
return "%.2f" % value
|
|
|
|
|
|
elif type == FieldStore.DataType.INTEGER:
|
|
|
|
|
|
return "%d" % value
|
|
|
|
|
|
elif type == FieldStore.DataType.PERCENT:
|
2026-01-23 23:44:21 +08:00
|
|
|
|
return ("%.1f" % (value * 100)) + "%"
|
2025-09-20 17:23:30 +08:00
|
|
|
|
elif type == FieldStore.DataType.ANGLE:
|
|
|
|
|
|
return "%.1f°" % value
|
2025-10-01 07:58:09 +08:00
|
|
|
|
elif type == FieldStore.DataType.FREQUENCY:
|
|
|
|
|
|
return "%.1fHz" % value
|
2025-09-20 17:23:30 +08:00
|
|
|
|
else:
|
|
|
|
|
|
return str(value)
|
2025-10-01 07:58:09 +08:00
|
|
|
|
func buildDescription(showNext: bool = false) -> String:
|
2025-09-20 17:23:30 +08:00
|
|
|
|
var next = update(level + 1, originalStore.duplicate(), UIState.player)
|
2025-09-05 22:23:41 +08:00
|
|
|
|
var result = descriptionTemplate
|
|
|
|
|
|
for key in store.keys():
|
2026-05-10 11:49:17 +08:00
|
|
|
|
var data = readStore(key)
|
2025-09-20 17:23:30 +08:00
|
|
|
|
var nextData = next[key]
|
2025-09-06 16:11:59 +08:00
|
|
|
|
var type = storeType.get(key, FieldStore.DataType.VALUE)
|
2025-09-20 17:23:30 +08:00
|
|
|
|
data = formatValue(data, type)
|
|
|
|
|
|
nextData = formatValue(nextData, type)
|
2025-10-01 07:58:09 +08:00
|
|
|
|
var text
|
|
|
|
|
|
if showNext:
|
|
|
|
|
|
text = "[color=cyan]%s[/color]→[color=yellow]%s[/color]" % [data, nextData]
|
|
|
|
|
|
else:
|
|
|
|
|
|
text = "[color=cyan]%s[/color]" % data
|
|
|
|
|
|
result = result.replace("$" + key, text)
|
2026-05-09 18:35:15 +08:00
|
|
|
|
return result
|
2026-05-10 11:49:17 +08:00
|
|
|
|
func rebuildDescription(showNext: bool):
|
|
|
|
|
|
descriptionLabel.text = buildDescription(showNext && (canUpdate(UIState.player) || canInlay()))
|
|
|
|
|
|
func rebuildSublimateOptions(showNext: bool):
|
|
|
|
|
|
for i in sublimateOptionsBox.get_children():
|
|
|
|
|
|
sublimateOptionsBox.remove_child(i)
|
|
|
|
|
|
for sublimate in getSublimateOptions():
|
|
|
|
|
|
var instance = ComponentManager.getUIComponent("SublimateOption").instantiate() as SublimateOptionHandler
|
|
|
|
|
|
instance.use = sublimate
|
|
|
|
|
|
instance.apply.connect(
|
|
|
|
|
|
func():
|
|
|
|
|
|
sublimate.apply(UIState.player, self )
|
|
|
|
|
|
rebuildBaseInfo()
|
|
|
|
|
|
rebuildDescription(showNext)
|
|
|
|
|
|
instance.rebuildInfo()
|
|
|
|
|
|
disruptSublimateOptions()
|
|
|
|
|
|
)
|
|
|
|
|
|
sublimateOptionsBox.add_child(instance)
|
|
|
|
|
|
disruptSublimateOptions()
|
2026-05-10 10:01:03 +08:00
|
|
|
|
func readStore(key: String):
|
|
|
|
|
|
return store.get(key, 0) + readStoreExtra(key)
|
2025-09-06 08:50:37 +08:00
|
|
|
|
func playSound(sound: String):
|
|
|
|
|
|
var body = sounds.get_node_or_null(sound)
|
|
|
|
|
|
if body is AudioStreamPlayer2D:
|
|
|
|
|
|
var cloned = body.duplicate() as AudioStreamPlayer2D
|
|
|
|
|
|
add_child(cloned)
|
|
|
|
|
|
cloned.play()
|
|
|
|
|
|
await cloned.finished
|
|
|
|
|
|
cloned.queue_free()
|
2026-02-05 20:52:04 +08:00
|
|
|
|
func canAttackBy(entity: EntityBase):
|
2026-03-07 09:05:36 +08:00
|
|
|
|
cooldownTimer.speedScale = entity.fields.get(FieldStore.Entity.ATTACK_SPEED) * attackSpeed
|
2026-03-14 07:21:42 +08:00
|
|
|
|
return cooldownTimer.isCooldowned() and entity.isEnergyEnough(needEnergy) and checkAttack(entity)
|
2026-02-05 20:52:04 +08:00
|
|
|
|
func tryAttack(entity: EntityBase):
|
2026-04-18 08:12:27 +08:00
|
|
|
|
if looping:
|
|
|
|
|
|
if checkAttack(entity):
|
|
|
|
|
|
return await attack(entity)
|
|
|
|
|
|
else:
|
|
|
|
|
|
exitLoop(entity)
|
|
|
|
|
|
else:
|
|
|
|
|
|
if canAttackBy(entity):
|
|
|
|
|
|
if emitType == EmitType.HOLD_LOOP:
|
|
|
|
|
|
var result = await loopStart(entity)
|
|
|
|
|
|
if result:
|
|
|
|
|
|
looping = true
|
|
|
|
|
|
cooldownTimer.start()
|
|
|
|
|
|
entity.useEnergy(needEnergy)
|
|
|
|
|
|
return result
|
|
|
|
|
|
else:
|
|
|
|
|
|
var result = await attack(entity)
|
|
|
|
|
|
if result:
|
|
|
|
|
|
cooldownTimer.start()
|
|
|
|
|
|
entity.useEnergy(needEnergy)
|
|
|
|
|
|
return result
|
2026-02-05 20:04:39 +08:00
|
|
|
|
func charged(base: float, percent: float):
|
2026-02-11 16:09:14 +08:00
|
|
|
|
return base * sqrt(1 + chargedTime / 15 * percent)
|
2026-04-18 08:12:27 +08:00
|
|
|
|
func exitLoop(entity: EntityBase):
|
|
|
|
|
|
if !looping: return
|
|
|
|
|
|
looping = false
|
|
|
|
|
|
loopExit(entity)
|
2026-05-10 10:01:03 +08:00
|
|
|
|
func addStoreExtra(key: String, value: float):
|
|
|
|
|
|
if !storeExtra.has(key):
|
|
|
|
|
|
storeExtra[key] = 0
|
|
|
|
|
|
storeExtra[key] += value
|
2026-05-10 11:49:17 +08:00
|
|
|
|
storeExtra[key] = clamp(storeExtra[key], 0, INF)
|
2026-05-10 10:01:03 +08:00
|
|
|
|
func readStoreExtra(key: String):
|
|
|
|
|
|
return storeExtra.get(key, 0)
|
2026-05-10 11:49:17 +08:00
|
|
|
|
func getSublimateOptions() -> Array[SublimateOption]:
|
|
|
|
|
|
if len(sublimateOptionsStored) == 0:
|
|
|
|
|
|
sublimateOptionsStored = sublimateOptions()
|
|
|
|
|
|
return sublimateOptionsStored
|
|
|
|
|
|
func disruptSublimateOptions():
|
|
|
|
|
|
var children = sublimateOptionsBox.get_children()
|
|
|
|
|
|
children.shuffle()
|
|
|
|
|
|
for index in len(children):
|
|
|
|
|
|
sublimateOptionsBox.remove_child(children[index])
|
|
|
|
|
|
for index in len(children):
|
|
|
|
|
|
var child = children[index]
|
|
|
|
|
|
if child is SublimateOptionHandler:
|
|
|
|
|
|
child.visible = index < 3
|
|
|
|
|
|
sublimateOptionsBox.add_child(child)
|
2025-09-05 22:23:41 +08:00
|
|
|
|
|
|
|
|
|
|
# 抽象
|
2026-05-10 10:01:03 +08:00
|
|
|
|
func sublimateOptions() -> Array[SublimateOption]:
|
|
|
|
|
|
return [] as Array[SublimateOption]
|
2025-09-06 11:14:02 +08:00
|
|
|
|
func update(_to: int, origin: Dictionary, _entity: EntityBase):
|
|
|
|
|
|
return origin
|
2026-04-18 08:12:27 +08:00
|
|
|
|
func loopStart(_entity: EntityBase):
|
|
|
|
|
|
pass
|
2026-03-14 07:21:42 +08:00
|
|
|
|
func checkAttack(_entity: EntityBase) -> bool:
|
|
|
|
|
|
return true
|
2025-09-05 22:23:41 +08:00
|
|
|
|
func attack(_entity: EntityBase):
|
|
|
|
|
|
pass
|
2026-04-18 08:12:27 +08:00
|
|
|
|
func loopExit(_entity: EntityBase):
|
|
|
|
|
|
pass
|