class_name PlayerArtUnlocks extends Node const PlayerWalletType = preload("res://economy/player_wallet.gd") signal unlocks_changed(unlock_mask: int) const BASE_BRUSH_SIZE: int = 1 const BASE_GRID_SIZE: int = 16 const BRUSH_SIZES: Array[int] = [1, 2, 3, 4] const GRID_SIZES: Array[int] = [16, 32, 64, 128] # Stable product IDs are both the save and shop transaction boundary. const PRODUCT_BITS: Dictionary[StringName, int] = { &"marker_ocean_teal": 0, &"marker_coral": 1, &"marker_sunny": 2, &"marker_leaf": 3, &"marker_blue": 4, &"marker_violet": 5, &"marker_charcoal": 6, &"brush_2x": 7, &"brush_3x": 8, &"brush_4x": 9, &"grid_32x": 10, &"grid_64x": 11, &"grid_128x": 12, } const COLOR_PRODUCTS: Dictionary[StringName, StringName] = { &"marker_ocean_teal": &"ocean_teal", &"marker_coral": &"coral", &"marker_sunny": &"sunny", &"marker_leaf": &"leaf", &"marker_blue": &"blue", &"marker_violet": &"violet", &"marker_charcoal": &"charcoal", } const BRUSH_PRODUCTS: Dictionary[StringName, int] = { &"brush_2x": 2, &"brush_3x": 3, &"brush_4x": 4, } const GRID_PRODUCTS: Dictionary[StringName, int] = { &"grid_32x": 32, &"grid_64x": 64, &"grid_128x": 128, } const ALL_UNLOCK_MASK: int = (1 << 13) - 1 var _unlock_mask: int = 0 func get_unlock_mask() -> int: return _unlock_mask func owns_product(product_id: StringName) -> bool: var bit: int = get_product_bit(product_id) return bit >= 0 and (_unlock_mask & (1 << bit)) != 0 func unlock_product(product_id: StringName) -> bool: var bit: int = get_product_bit(product_id) if bit < 0 or owns_product(product_id): return false _unlock_mask |= 1 << bit unlocks_changed.emit(_unlock_mask) return true func purchase_product( product_id: StringName, wallet: PlayerWalletType, cost: int, ) -> bool: if ( wallet == null or cost < 0 or owns_product(product_id) or get_product_bit(product_id) < 0 or not wallet.can_afford(cost) ): return false if not wallet.debit(cost): return false if unlock_product(product_id): return true if not wallet.credit(cost): push_error("Art upgrade purchase rollback failed.") return false func restore_mask(value: int) -> bool: if value < 0 or (value & ~ALL_UNLOCK_MASK) != 0: return false var changed: bool = _unlock_mask != value _unlock_mask = value if changed: unlocks_changed.emit(_unlock_mask) return true func reset_to_defaults() -> void: restore_mask(0) func to_save_data() -> Dictionary: return {"unlock_mask": _unlock_mask} func get_unlocked_color_ids() -> Array[StringName]: var result: Array[StringName] = [SurfaceDrawingPalette.DEFAULT_COLOR_ID] for product_id: StringName in COLOR_PRODUCTS: if owns_product(product_id): result.append(StringName(str(COLOR_PRODUCTS[product_id]))) return result func get_unlocked_brush_sizes() -> Array[int]: var result: Array[int] = [BASE_BRUSH_SIZE] for product_id: StringName in BRUSH_PRODUCTS: if owns_product(product_id): result.append(int(BRUSH_PRODUCTS[product_id])) result.sort() return result func get_unlocked_grid_sizes() -> Array[int]: var result: Array[int] = [BASE_GRID_SIZE] for product_id: StringName in GRID_PRODUCTS: if owns_product(product_id): result.append(int(GRID_PRODUCTS[product_id])) result.sort() return result func is_color_unlocked(color_id: StringName) -> bool: return color_id in get_unlocked_color_ids() func is_brush_size_unlocked(brush_size: int) -> bool: return brush_size in get_unlocked_brush_sizes() func is_grid_size_unlocked(grid_size: int) -> bool: return grid_size in get_unlocked_grid_sizes() static func get_product_bit(product_id: StringName) -> int: return int(PRODUCT_BITS.get(product_id, -1)) static func is_product_id(product_id: StringName) -> bool: return PRODUCT_BITS.has(product_id) static func resulting_mask(current_mask: int, product_id: StringName) -> int: var bit: int = get_product_bit(product_id) if bit < 0: return current_mask return current_mask | (1 << bit) static func color_id_for_product(product_id: StringName) -> StringName: return StringName(str(COLOR_PRODUCTS.get(product_id, ""))) static func brush_size_for_product(product_id: StringName) -> int: return int(BRUSH_PRODUCTS.get(product_id, -1)) static func grid_size_for_product(product_id: StringName) -> int: return int(GRID_PRODUCTS.get(product_id, -1))