extends CanvasLayer ## The quartermaster: spend a level-up, and review what you have taken. ## ## The three options are the SERVER's, held on the character until one is ## spent -- closing and reopening this screen shows the same three, because ## otherwise it would be a reroll button and everyone would press it until a ## legendary appeared. ## ## The brief asked for a separate screen listing upgrades already taken. It is ## the lower half of this one instead: the two are read together (what do I ## have, what should I add) and splitting them would mean closing one panel to ## answer a question raised by the other. signal choose_requested(index: int) signal closed const RARITY_COLOURS := { UpgradeDef.Rarity.COMMON: Color(0.72, 0.76, 0.84), UpgradeDef.Rarity.UNCOMMON: Color(0.55, 0.85, 0.6), UpgradeDef.Rarity.RARE: Color(0.55, 0.7, 1.0), UpgradeDef.Rarity.LEGENDARY: Color(1.0, 0.72, 0.3), } var _title: Label var _cards: HBoxContainer var _taken: Label var _summary: Label var _status: Label func _ready() -> void: layer = 28 visible = false var root := Control.new() root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(root) var scrim := ColorRect.new() scrim.color = Color(0.03, 0.03, 0.06, 0.9) scrim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_child(scrim) var centre := CenterContainer.new() centre.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_child(centre) var panel := VBoxContainer.new() panel.custom_minimum_size = Vector2(760.0, 0.0) panel.add_theme_constant_override("separation", 10) centre.add_child(panel) _title = Label.new() _title.add_theme_font_size_override("font_size", 26) panel.add_child(_title) _cards = HBoxContainer.new() _cards.add_theme_constant_override("separation", 10) panel.add_child(_cards) _status = Label.new() _status.add_theme_color_override("font_color", Color(1.0, 0.6, 0.5)) panel.add_child(_status) panel.add_child(_rule()) var taken_title := Label.new() taken_title.text = "ALREADY TAKEN" taken_title.add_theme_font_size_override("font_size", 15) taken_title.add_theme_color_override("font_color", Color(0.6, 0.65, 0.78)) panel.add_child(taken_title) _taken = Label.new() _taken.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _taken.custom_minimum_size = Vector2(740.0, 0.0) panel.add_child(_taken) _summary = Label.new() _summary.add_theme_color_override("font_color", Color(0.65, 0.72, 0.85)) _summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _summary.custom_minimum_size = Vector2(740.0, 0.0) panel.add_child(_summary) var close := Button.new() close.text = "Close (E)" close.custom_minimum_size = Vector2(160.0, 34.0) close.pressed.connect(func() -> void: closed.emit()) panel.add_child(close) func _rule() -> Control: var line := ColorRect.new() line.color = Color(0.2, 0.22, 0.3) line.custom_minimum_size = Vector2(0.0, 1.0) return line func set_status(text: String) -> void: _status.text = text ## Rebuild from the server's numbers. Called on every upgrade change, so the ## screen can never show an offer that has already been spent. func refresh(pending: int, offer: Array[StringName], taken: Array[StringName]) -> void: _status.text = "" if pending > 0: _title.text = "CHOOSE AN UPGRADE (%d waiting)" % pending else: _title.text = "QUARTERMASTER — nothing to spend" for child in _cards.get_children(): child.queue_free() if pending > 0: for i in offer.size(): _cards.add_child(_make_card(i, offer[i])) _taken.text = _taken_text(taken) _summary.text = _summary_text(taken) func _make_card(index: int, id: StringName) -> Control: var def := Upgrades.get_def(id) var card := VBoxContainer.new() card.custom_minimum_size = Vector2(240.0, 0.0) card.add_theme_constant_override("separation", 6) if def == null: return card var tint: Color = RARITY_COLOURS.get(def.rarity, Color.WHITE) var name_label := Label.new() name_label.text = def.display_name name_label.add_theme_font_size_override("font_size", 18) name_label.add_theme_color_override("font_color", tint) card.add_child(name_label) var rarity := Label.new() rarity.text = UpgradeDef.rarity_name(def.rarity).to_upper() rarity.add_theme_font_size_override("font_size", 11) rarity.add_theme_color_override("font_color", Color(tint, 0.7)) card.add_child(rarity) var body := Label.new() body.text = def.description body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART body.custom_minimum_size = Vector2(230.0, 96.0) body.add_theme_font_size_override("font_size", 13) card.add_child(body) # Shown on every card because it applies to every card. Deliberately NOT # labelled "every upgrade": that a flat bonus rides along with all of them # is a design principle, and the player only needs the number. var bonus := Label.new() bonus.text = "+%d%% damage" % roundi(SimConfig.UPGRADE_DAMAGE_BONUS * 100.0) bonus.add_theme_font_size_override("font_size", 12) bonus.add_theme_color_override("font_color", Color(0.55, 0.8, 0.6)) card.add_child(bonus) var take := Button.new() take.text = "Take" take.custom_minimum_size = Vector2(0.0, 34.0) take.pressed.connect(func() -> void: choose_requested.emit(index)) card.add_child(take) return card ## Counted rather than listed one per line: "Split Shot x3" is the number that ## matters, and a level 15 character has fourteen of these. func _taken_text(taken: Array[StringName]) -> String: if taken.is_empty(): return "nothing yet" var counts := {} var order: Array[StringName] = [] for id in taken: if not counts.has(id): counts[id] = 0 order.append(id) counts[id] += 1 var parts: Array[String] = [] for id in order: var def := Upgrades.get_def(id) var name_text := def.display_name if def != null else String(id) parts.append("%s x%d" % [name_text, int(counts[id])] if int(counts[id]) > 1 else name_text) return " ".join(parts) ## What the upgrades actually add up to. Built through PlayerStats, the same ## class the server fires with, so this cannot drift from the real numbers. func _summary_text(taken: Array[StringName]) -> String: var s := PlayerStats.build(taken) var shots := 1 + s.side_shots + s.parallel_shots var per_second := float(SimConfig.TICK_RATE) / float(s.fire_cooldown) var parts: Array[String] = [ "%d damage per shot" % s.damage, "%.1f shots/sec" % per_second, "%d projectile%s per shot" % [shots, "" if shots == 1 else "s"], "%d bullet speed%s" % [roundi(s.bullet_speed), " (capped)" if s.speed_capped else ""], ] if s.max_hp_mult != 1.0: parts.append("%d%% max health" % roundi(s.max_hp_mult * 100.0)) if s.split_charges > 0: parts.append("splits %dx" % s.split_charges) if s.poison_fraction > 0.0: parts.append("+%d%% as poison" % roundi(s.poison_fraction * 100.0)) if s.erase_chance > 0.0: parts.append("%.1f%% erase" % (s.erase_chance * 100.0)) return " · ".join(parts)