@tool extends PanelContainer class_name Weapon enum EmitType { HOLD_SHOOT, CLICK_SHOOT, CHARGE, HOLD_LOOP } @export var avatarTexture: Texture2D = null @export var displayName: String = "未命名武器" @export var quality: WeaponName.Quality = WeaponName.Quality.COMMON @export var typeTopic: WeaponName.TypeTopic = WeaponName.TypeTopic.IMPACT @export var soulLevel: int = 1 @export var costBeachball: int = 500 @export var emitType: EmitType = EmitType.HOLD_SHOOT @export var store: Dictionary = { "atk": 10 } @export var storeType: Dictionary = { "atk": FieldStore.DataType.INTEGER } @export_multiline var descriptionTemplate: String = "造成$atk点伤害。" @export var sources: Array[String] = [] @export var tease: String = "" @export var needEnergy: float = 0 @export var cooldown: float = 100 @export var debugRebuild: bool = false @export var level: int = 0 @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 @onready var sublimateOptionsBox: Control = $%sublimateOptions var cooldownTimer: CooldownTimer = null var originalStore: Dictionary = {} var chargedTime: float = 0 var attackSpeed: float = 1 var looping: bool = false var autoUpdate: bool = false var storeExtra: Dictionary = {} var sublimateOptionsStored: Array[SublimateOption] = [] func _ready(): cooldownTimer = CooldownTimer.new() cooldownTimer.cooldown = cooldown originalStore = store 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) ) updateBtn.pressed.connect( func(): updateApply(UIState.player) ) extractBtn.pressed.connect( func(): if soulLevel > WeaponName.SoulLevel.NORMALIZE: UIState.player.getItem({ ItemStore.ItemType.SOUL: soulLevel - 1 }) soulLevel -= 1 updateStore(level, UIState.player) rebuildInfo() else: UIState.showTip("[b]%s[/b]还未镶嵌任何灵魂!" % displayName, TipBox.MessageType.ERROR) ) inlayBtn.pressed.connect( func(): if soulLevel < WeaponName.SoulLevel.INFINITY: if UIState.player.useItem({ ItemStore.ItemType.SOUL: soulLevel }): soulLevel += 1 updateStore(level, UIState.player) rebuildInfo() else: UIState.showTip("持有的灵魂数量不足!", TipBox.MessageType.ERROR) else: UIState.showTip("[b]%s[/b]的灵魂槽位已满!" % displayName, TipBox.MessageType.ERROR) ) moveLeftBtn.pressed.connect( func(): if get_parent(): var myIndex = get_index() var leftIndex = max(myIndex - 1, 0) get_parent().move_child(self , leftIndex) ArrayTool.swap(UIState.player.weapons, myIndex, leftIndex) UIState.player.rebuildWeaponIcons() ) moveRightBtn.pressed.connect( func(): if get_parent(): var myIndex = get_index() var rightIndex = min(myIndex + 1, get_parent().get_child_count() - 1) get_parent().move_child(self , rightIndex) ArrayTool.swap(UIState.player.weapons, myIndex, rightIndex) UIState.player.rebuildWeaponIcons() ) for i in sounds.get_children(): i.process_mode = ProcessMode.PROCESS_MODE_ALWAYS debugRebuild = false # 只能在编辑器里打开 rebuildInfo() func _physics_process(_delta): if debugRebuild: rebuildInfo() func canUpdate(entity: EntityBase): return entity.hasItem({ItemStore.ItemType.BEACHBALL: costBeachball}) func canInlay(): return UIState.player.hasItem({ItemStore.ItemType.SOUL: soulLevel}) func updateApply(entity: EntityBase) -> bool: if canUpdate(entity): level += 1 entity.inventory[ItemStore.ItemType.BEACHBALL] -= costBeachball updateStore(level, entity) costBeachball = floor(GameRule.weaponUpdateCost * costBeachball) rebuildInfo(true) return true else: UIState.showTip("沙滩球不足!", TipBox.MessageType.ERROR) return false func updateStore(to: int, entity: EntityBase): store = update(to, originalStore.duplicate(), entity) func multipiler() -> float: if is_instance_valid(UIState.player): return 1 - UIState.player.fields.get(FieldStore.Entity.PRICE_REDUCTION) else: return 1 func rebuildInfo(showNext: bool = false): rebuildBaseInfo() rebuildDescription(showNext) rebuildSublimateOptions(showNext) func rebuildBaseInfo(): avatarRect.texture = avatarTexture nameLabel.displayName = displayName nameLabel.quality = quality nameLabel.typeTopic = typeTopic nameLabel.soulLevel = soulLevel nameLabel.level = level sourceLabel.text = " × ".join(sources) if len(tease) > 0: teaseLabel.text = "“%s”" % tease teaseLabel.show() else: teaseLabel.hide() energyLabel.text = "%.1f" % needEnergy beachball.count = costBeachball soul.count = soulLevel if is_instance_valid(UIState.player): beachball.enough = canUpdate(UIState.player) soul.enough = canInlay() 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: return ("%.1f" % (value * 100)) + "%" elif type == FieldStore.DataType.ANGLE: return "%.1f°" % value elif type == FieldStore.DataType.FREQUENCY: return "%.1fHz" % value else: return str(value) func buildDescription(showNext: bool = false) -> String: var next = update(level + 1, originalStore.duplicate(), UIState.player) var result = descriptionTemplate for key in store.keys(): var data = readStore(key) var nextData = next[key] var type = storeType.get(key, FieldStore.DataType.VALUE) data = formatValue(data, type) nextData = formatValue(nextData, type) 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) return result 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() func readStore(key: String): return store.get(key, 0) + readStoreExtra(key) 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() func canAttackBy(entity: EntityBase): cooldownTimer.speedScale = entity.fields.get(FieldStore.Entity.ATTACK_SPEED) * attackSpeed return cooldownTimer.isCooldowned() and entity.isEnergyEnough(needEnergy) and checkAttack(entity) func tryAttack(entity: EntityBase): 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 func charged(base: float, percent: float): return base * sqrt(1 + chargedTime / 15 * percent) func exitLoop(entity: EntityBase): if !looping: return looping = false loopExit(entity) func addStoreExtra(key: String, value: float): if !storeExtra.has(key): storeExtra[key] = 0 storeExtra[key] += value storeExtra[key] = clamp(storeExtra[key], 0, INF) func readStoreExtra(key: String): return storeExtra.get(key, 0) 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) # 抽象 func sublimateOptions() -> Array[SublimateOption]: return [] as Array[SublimateOption] func update(_to: int, origin: Dictionary, _entity: EntityBase): return origin func loopStart(_entity: EntityBase): pass func checkAttack(_entity: EntityBase) -> bool: return true func attack(_entity: EntityBase): pass func loopExit(_entity: EntityBase): pass