Last active
January 23, 2025 03:03
-
-
Save drewhamlett/b4c163cc6acd05749df98b8a5ef0c4d1 to your computer and use it in GitHub Desktop.
enemy.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| class Entity | |
| def initialize(x, y) | |
| @default_fade_time = 50 | |
| @fade_timer = nil | |
| @default_pulse_timer = Utils.random(100, 150) | |
| @pulse_timer = @default_pulse_timer | |
| end | |
| def fade_complete? | |
| return if @fade_timer.nil? | |
| @fade_timer == 0 | |
| end | |
| def fading? | |
| return false if @fade_timer.nil? | |
| @fade_timer > 0 | |
| end | |
| def fade | |
| @fade_timer = @default_fade_time | |
| end | |
| protected | |
| def alpha(default = 255) | |
| fading? ? (@fade_timer * 2.55).to_i : default | |
| end | |
| def update | |
| unless @fade_timer.nil? | |
| @fade_timer -= 1 if @fade_timer > 0 | |
| end | |
| @pulse_direction ||= -1 | |
| @pulse_timer += @pulse_direction | |
| @pulse_direction *= -1 if @pulse_timer <= 60 || @pulse_timer >= @default_pulse_timer | |
| end | |
| end | |
| class Enemy < Entity | |
| attr_gtk | |
| ENEMY_MAX_SPEED = 1 | |
| ENEMY_ACCELERATION = 0.1 | |
| ENEMY_SEPARATION_RADIUS = 30 | |
| ENEMY_SEPARATION_FORCE = 0.5 | |
| attr_reader :x, :y, :w, :h, :id, :default_color, :health | |
| attr_accessor :velocity_x, :velocity_y, :stunned_until, :destroyed, :velocity_hit_at | |
| FRICTION = 0.94 | |
| SQUASH_DURATION = 50 | |
| SQUASH_SCALE = 0.8 | |
| STUN_DURATION = 120 # 2 seconds at 60fps | |
| # @param x [Integer] | |
| # @param y [Integer] | |
| def initialize(x, y) | |
| super | |
| @health = 7 | |
| @hit_count = 0 | |
| @size = [10, 12, 14, 16].sample | |
| @id = object_id | |
| @x = x | |
| @y = y | |
| @w = @size | |
| @h = @size | |
| @target_w = @size | |
| @target_h = @size | |
| @velocity_x = 0 | |
| @velocity_y = 0 | |
| @flash_timer = 0 | |
| @squash_timer = 0 | |
| @squash_start_at = 0 | |
| @stunned_until = 0 | |
| @default_color = [ | |
| Utils.colors[:yellow], | |
| Utils.colors[:purple], | |
| Utils.colors[:green], | |
| Utils.colors[:blue], | |
| Utils.colors[:red], | |
| Utils.colors[:light] | |
| ].sample | |
| @destroyed = false | |
| @hit_timer = 0 | |
| @default_angle = [0, 90, 180, 270].sample | |
| @velocity_hit_at = { | |
| x: 0, | |
| y: 0 | |
| } | |
| @max_health = 3 | |
| @health = @max_health | |
| end | |
| def self.update_all | |
| Array.reject!(GTK.args.state.enemies) { |enemy| enemy.dead? } | |
| i = 0 | |
| len = GTK.args.state.enemies.length | |
| player = GTK.args.state.player | |
| while i < len | |
| enemy = GTK.args.state.enemies[i] | |
| unless enemy&.stunned? | |
| dx = player.x - enemy.x | |
| dy = player.y - enemy.y | |
| dist_squared = dx * dx + dy * dy | |
| if dist_squared > 0 | |
| inv_dist = 1.0 / Math.sqrt(dist_squared) | |
| enemy.velocity_x += dx * inv_dist * ENEMY_ACCELERATION | |
| enemy.velocity_y += dy * inv_dist * ENEMY_ACCELERATION | |
| end | |
| if Kernel.tick_count % 4 == 0 | |
| # Check only a few random nearby enemies | |
| force_x = 0 | |
| force_y = 0 | |
| check_count = 0 | |
| j = 0 | |
| while j < len && check_count < 5 | |
| other = GTK.args.state.enemies[j] | |
| next if other.nil? | |
| if other.id != enemy.id && !other.stunned? | |
| dx = enemy.x - other.x | |
| dy = enemy.y - other.y | |
| dist_squared = dx * dx + dy * dy | |
| if dist_squared < ENEMY_SEPARATION_RADIUS * ENEMY_SEPARATION_RADIUS && dist_squared > 0 | |
| check_count += 1 | |
| force_x += dx * 0.01 | |
| force_y += dy * 0.01 | |
| end | |
| end | |
| j += 1 | |
| end | |
| enemy.velocity_x += force_x | |
| enemy.velocity_y += force_y | |
| end | |
| speed_squared = enemy.velocity_x * enemy.velocity_x + enemy.velocity_y * enemy.velocity_y | |
| if speed_squared > ENEMY_MAX_SPEED * ENEMY_MAX_SPEED | |
| speed = Math.sqrt(speed_squared) | |
| enemy.velocity_x = (enemy.velocity_x / speed) * ENEMY_MAX_SPEED | |
| enemy.velocity_y = (enemy.velocity_y / speed) * ENEMY_MAX_SPEED | |
| end | |
| end | |
| enemy&.update | |
| i += 1 | |
| end | |
| end | |
| def stunned? | |
| GTK.args.state.tick_count < @stunned_until | |
| end | |
| def hit | |
| @hit_timer = 50 | |
| @hit_count += 1 | |
| @health -= 1 | |
| end | |
| def hit? | |
| @hit_timer > 0 | |
| end | |
| def stun | |
| @stunned_until = state.tick_count + STUN_DURATION | |
| end | |
| def destroy | |
| fade | |
| end | |
| def dead? | |
| @destroyed | |
| end | |
| def update | |
| super | |
| @hit_timer -= 1 if @hit_timer > 0 | |
| apply_physics | |
| handle_effects | |
| if fade_complete? && !dead? | |
| @destroyed = true | |
| end | |
| if dead? && Utils.random(1, 2) == 1 | |
| PowerUp.spawn(@x, @y) | |
| end | |
| @x = Utils.clamp(@x, 0, WORLD_WIDTH) | |
| @y = Utils.clamp(@y, 0, WORLD_HEIGHT) | |
| end | |
| def apply_physics | |
| @x += @velocity_x | |
| @y += @velocity_y | |
| @velocity_x *= FRICTION | |
| @velocity_y *= FRICTION | |
| end | |
| def handle_effects | |
| @flash_timer -= 1 | |
| @squash_timer -= 1 if @squash_timer > 0 | |
| if @squash_timer > 0 | |
| progress = @squash_start_at.ease(SQUASH_DURATION, [:flip, :cube, :flip]) | |
| scale = 1 - (1 - SQUASH_SCALE) * (1 - progress) | |
| @w = @size * scale | |
| @h = @size * scale | |
| else | |
| @w = @size | |
| @h = @size | |
| end | |
| end | |
| def squash | |
| @squash_timer = SQUASH_DURATION | |
| @squash_start_at = state.tick_count | |
| end | |
| def flash | |
| @flash_timer = 10 | |
| @flash_timer -= 1 | |
| if @flash_timer.zero? | |
| @flash_timer = 10 | |
| end | |
| end | |
| def draw | |
| return [] if dead? | |
| glow = { | |
| x: @x + @size / 2, | |
| y: @y + @size / 2, | |
| w: @w * @pulse_timer / 30, | |
| h: @h * @pulse_timer / 30, | |
| anchor_x: 0.5, | |
| anchor_y: 0.5, | |
| path: Sprites.path, | |
| **Sprites.get("glow_pixel.png"), | |
| a: alpha(200), | |
| **color | |
| } | |
| health_bar = { | |
| x: @x, | |
| y: @y + @h + 1, | |
| w: @w * (@health / @max_health), | |
| h: 1.6, | |
| path: Sprites.path, | |
| **Sprites.get("particle.png"), | |
| **color, | |
| a: 100 | |
| } | |
| image_config = if @hit_count == 0 | |
| Sprites.get("block.png") | |
| elsif @hit_count <= 9 | |
| Sprites.get("block_hit_#{@hit_count}.png") | |
| else | |
| Sprites.get("block_hit_9.png") | |
| end | |
| [ | |
| { | |
| x: @x, | |
| y: @y, | |
| w: @w, | |
| h: @h, | |
| **image_config, | |
| path: Sprites.path, | |
| **color, | |
| angle: @default_angle, | |
| a: alpha(255) | |
| }, | |
| glow, | |
| health_bar | |
| ] | |
| end | |
| private | |
| def color | |
| if @flash_timer > 0 | |
| Utils.colors[:white] | |
| else | |
| @default_color | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require_relative "utils" | |
| require_relative "core/sprites" | |
| require_relative "player" | |
| require_relative "particles" | |
| require_relative "enemy" | |
| require_relative "noise" | |
| require_relative "screen_shake" | |
| require_relative "core/hit_label" | |
| require_relative "core/map" | |
| require_relative "core/pickup" | |
| require_relative "core/box" | |
| require_relative "collision" | |
| require_relative "core/bar" | |
| require_relative "core/shockwave" | |
| require_relative "core/sound" | |
| require_relative "core/combo_label" | |
| require_relative "core/base" | |
| require_relative "screens/pause_screen" | |
| require_relative "screens/start_screen" | |
| require_relative "core/power_up" | |
| class Class | |
| def r | |
| $gtk.reset | |
| $game = Game.new(args) | |
| end | |
| def disable_fx | |
| GTK.args.state.fx = false | |
| end | |
| def enable_fx | |
| GTK.args.state.fx = true | |
| end | |
| def clear_enemies | |
| state.enemies = [] | |
| end | |
| def spawn_enemies(count = 100) | |
| count.times do | |
| state.enemies << Enemy.new( | |
| Utils.random(0, WORLD_WIDTH), | |
| Utils.random(0, WORLD_HEIGHT) | |
| ) | |
| end | |
| end | |
| end | |
| FONT = "fonts/TinyUnicode.ttf".freeze | |
| FONT_SIZE = 2 | |
| # Physical screen dimensions | |
| SCREEN_WIDTH = 1280 | |
| SCREEN_HEIGHT = 720 | |
| # DEBUG = !GTK.production? | |
| DEBUG = true | |
| SCALE = 3 | |
| # Target game resolution | |
| TARGET_WIDTH = (SCREEN_WIDTH / SCALE).freeze | |
| TARGET_HEIGHT = (SCREEN_HEIGHT / SCALE).freeze | |
| GAME_ZOOM = [ | |
| (SCREEN_WIDTH / TARGET_WIDTH).floor, | |
| (SCREEN_HEIGHT / TARGET_HEIGHT).floor | |
| ].min.freeze | |
| GAME_X_OFFSET = (SCREEN_WIDTH - (TARGET_WIDTH * GAME_ZOOM)).idiv(2).freeze | |
| GAME_Y_OFFSET = (SCREEN_HEIGHT - (TARGET_HEIGHT * GAME_ZOOM)).idiv(2).freeze | |
| WORLD_WIDTH = 1000 | |
| WORLD_HEIGHT = 1000 | |
| MINIMAP_SIZE = 35 | |
| MINIMAP_PADDING = 6 | |
| MINIMAP_UPDATE_FREQUENCY = 30 | |
| TARGET_FPS = 30 | |
| class Game | |
| attr_gtk | |
| CAMERA_LERP = 0.04 | |
| CAMERA_OFFSET_X = (TARGET_WIDTH / 2) | |
| CAMERA_OFFSET_Y = (TARGET_HEIGHT / 2) | |
| def initialize(args) | |
| @player = Player.new | |
| @grid = {} | |
| @noise = Noise.new | |
| @bar = Bar.new(args) | |
| @particles = Particles.new(args) | |
| @boxes = 5.times.map do | |
| Box.new(Utils.random(0, TARGET_WIDTH), Utils.random(0, TARGET_HEIGHT)) | |
| end | |
| @camera = { | |
| x: 0, | |
| y: 0, | |
| target_x: 0, | |
| target_y: 0 | |
| } | |
| args.render_target(:minimap).width = MINIMAP_SIZE | |
| args.render_target(:minimap).height = MINIMAP_SIZE | |
| @last_minimap_update = 0 | |
| end | |
| private | |
| def tick | |
| state.camera ||= @camera | |
| state.player = @player | |
| state.map ||= Map.new(args) | |
| state.pickups ||= [] | |
| state.enemies ||= [] | |
| state.bases ||= [Base.new(400, 400), Base.new(100, 100), Base.new(800, 100)] | |
| state.noise_intensity ||= 15 | |
| @player.args = args | |
| Shockwave.init | |
| ComboLabel.init(args) | |
| HitLabel.init(args) | |
| if Kernel.tick_count.zero? | |
| 10.times do | |
| state.enemies << Enemy.new( | |
| Utils.random(0, TARGET_WIDTH), | |
| Utils.random(0, TARGET_HEIGHT) | |
| ) | |
| end | |
| end | |
| Array.each(state.enemies) do |enemy| | |
| enemy.args = args | |
| end | |
| update | |
| draw | |
| end | |
| def draw | |
| if state.shake_timer > 0 | |
| shake_x = rand(2 * state.shake_intensity + 1) - state.shake_intensity | |
| shake_y = rand(2 * state.shake_intensity + 1) - state.shake_intensity | |
| state.shake_timer -= 1 | |
| else | |
| shake_x = 0 | |
| shake_y = 0 | |
| end | |
| args.render_target(:game).width = TARGET_WIDTH | |
| args.render_target(:game).height = TARGET_HEIGHT | |
| args.render_target(:game).background_color = Utils.colors[:black] | |
| args.outputs.background_color = Utils.colors[:black] | |
| args.render_target(:overlay).width = TARGET_WIDTH | |
| args.render_target(:overlay).height = TARGET_HEIGHT | |
| args.render_target(:overlay).background_color = Utils.colors[:black] | |
| args.render_target(:game).sprites << [ | |
| Array.map(@particles.draw(args)) do |p| | |
| p.merge(x: p.x - @camera.x, y: p.y - @camera.y) | |
| end | |
| ] | |
| enemies_to_render = if state.tick_count % 4 == 0 | |
| Array.select(state.enemies) do |e| | |
| e.x >= @camera.x - e.w && | |
| e.x <= @camera.x + TARGET_WIDTH - 5 && | |
| e.y >= @camera.y - e.h && | |
| e.y <= @camera.y + TARGET_HEIGHT - 5 | |
| end | |
| else | |
| @last_enemies_to_render ||= [] | |
| end | |
| @last_enemies_to_render = enemies_to_render if state.tick_count % 3 == 0 | |
| args.render_target(:game).sprites << [ | |
| Array.flat_map(state.pickups) do |pi| | |
| Array.map(pi.draw) do |sprite| | |
| sprite.merge(x: sprite.x - @camera.x, y: sprite.y - @camera.y) | |
| end | |
| end, | |
| Array.map(@player.draw) do |sprite| | |
| sprite.merge(x: sprite.x - @camera.x, y: sprite.y - @camera.y) | |
| end, | |
| Array.flat_map(enemies_to_render) do |e| | |
| Array.map(e.draw) do |sprite| | |
| sprite.merge(x: sprite.x - @camera.x, y: sprite.y - @camera.y) | |
| end | |
| end, | |
| Array.flat_map(@boxes) do |b| | |
| Array.map(b.draw) do |sprite| | |
| sprite.merge(x: sprite.x - @camera.x, y: sprite.y - @camera.y) | |
| end | |
| end, | |
| Array.map(Shockwave.draw) do |p| | |
| p.merge(x: p.x - @camera.x, y: p.y - @camera.y) | |
| end, | |
| @noise | |
| ] | |
| args.outputs.labels << [ | |
| Array.map(HitLabel.draw(args)) do |p| | |
| p.merge( | |
| x: (p.x - @camera.x) * GAME_ZOOM + GAME_X_OFFSET, | |
| y: (p.y - @camera.y) * GAME_ZOOM + GAME_Y_OFFSET | |
| ) | |
| end, | |
| ComboLabel.draw, | |
| { | |
| x: 20, | |
| y: 40, | |
| text: "Score: 100", | |
| font: FONT, | |
| size_enum: 4, | |
| a: 255, | |
| **Utils.colors[:white] | |
| } | |
| ] | |
| args.outputs.sprites << [ | |
| { | |
| x: shake_x, | |
| y: shake_y, | |
| w: SCREEN_WIDTH, | |
| h: SCREEN_HEIGHT, | |
| path: :game, | |
| angle: 1 | |
| }, | |
| # { | |
| # x: GAME_X_OFFSET, | |
| # y: GAME_Y_OFFSET, | |
| # w: TARGET_WIDTH * GAME_ZOOM, | |
| # h: TARGET_HEIGHT * GAME_ZOOM, | |
| # path: :overlay, | |
| # blendmode_enum: 4, | |
| # a: 0 | |
| # }, | |
| @bar.draw | |
| ] | |
| if args.state.minimap_enabled | |
| if state.tick_count - @last_minimap_update >= MINIMAP_UPDATE_FREQUENCY | |
| update_minimap | |
| @last_minimap_update = state.tick_count | |
| end | |
| args.render_target(:game).primitives << [ | |
| { | |
| x: TARGET_WIDTH - MINIMAP_SIZE - MINIMAP_PADDING, | |
| y: TARGET_HEIGHT - MINIMAP_SIZE - MINIMAP_PADDING, | |
| w: MINIMAP_SIZE, | |
| h: MINIMAP_SIZE, | |
| r: 0, g: 0, b: 0, a: 40, | |
| primitive_marker: :solid | |
| }, | |
| { | |
| x: TARGET_WIDTH - MINIMAP_SIZE - MINIMAP_PADDING, | |
| y: TARGET_HEIGHT - MINIMAP_SIZE - MINIMAP_PADDING, | |
| w: MINIMAP_SIZE, | |
| h: MINIMAP_SIZE, | |
| path: :minimap, | |
| primitive_marker: :sprite | |
| }, | |
| { | |
| x: TARGET_WIDTH - MINIMAP_SIZE - MINIMAP_PADDING + (@player.x / WORLD_WIDTH * MINIMAP_SIZE), | |
| y: TARGET_HEIGHT - MINIMAP_SIZE - MINIMAP_PADDING + (@player.y / WORLD_HEIGHT * MINIMAP_SIZE), | |
| w: 2, | |
| h: 2, | |
| r: 255, g: 255, b: 255, | |
| primitive_marker: :solid | |
| }, | |
| { | |
| x: TARGET_WIDTH - MINIMAP_SIZE - MINIMAP_PADDING, | |
| y: TARGET_HEIGHT - MINIMAP_SIZE - MINIMAP_PADDING, | |
| w: MINIMAP_SIZE, | |
| h: MINIMAP_SIZE, | |
| r: 255, g: 255, b: 255, | |
| primitive_marker: :border | |
| } | |
| ] | |
| end | |
| end | |
| def update | |
| handle_collisions | |
| Enemy.update_all | |
| @bar.update | |
| @player.update | |
| ScreenShake.tick(args) | |
| @particles.tick(args) | |
| HitLabel.tick(args) | |
| Shockwave.update | |
| Sound.tick | |
| ComboLabel.tick | |
| Array.each(@boxes) do |box| | |
| box.update | |
| # box.delete_if(&:dead?) | |
| end | |
| update_camera | |
| update_enemies | |
| args.state.map.tick if args.state.fx | |
| end | |
| def update_camera | |
| # Set camera target to follow player | |
| @camera.target_x = @player.x - CAMERA_OFFSET_X | |
| @camera.target_y = @player.y - CAMERA_OFFSET_Y | |
| # Smooth camera movement using lerp | |
| dx = @camera.target_x - @camera.x | |
| dy = @camera.target_y - @camera.y | |
| @camera.x += dx * CAMERA_LERP | |
| @camera.y += dy * CAMERA_LERP | |
| # Clamp camera position to world bounds | |
| @camera.x = Utils.clamp(@camera.x, 0, WORLD_WIDTH - TARGET_WIDTH) | |
| @camera.y = Utils.clamp(@camera.y, 0, WORLD_HEIGHT - TARGET_HEIGHT) | |
| end | |
| def update_enemies | |
| # Utils.every(3.seconds) do | |
| # state.enemies << Enemy.new( | |
| # Utils.random(@camera.x + TARGET_WIDTH, WORLD_WIDTH), | |
| # Utils.random(@camera.y + TARGET_HEIGHT, WORLD_HEIGHT) | |
| # ) | |
| # end | |
| end | |
| def handle_collisions | |
| Collision.check( | |
| args, | |
| player: @player, | |
| pickups: state.pickups, | |
| boxes: @boxes, | |
| enemies: state.enemies | |
| ) | |
| end | |
| def get_mouse_position | |
| x = ((args.mouse.x - GAME_X_OFFSET) / GAME_ZOOM).floor + @camera.x | |
| y = ((args.mouse.y - GAME_Y_OFFSET) / GAME_ZOOM).floor + @camera.y | |
| [x, y] | |
| end | |
| def update_minimap | |
| args.render_target(:minimap).primitives.clear | |
| args.render_target(:minimap).primitives << Array.map(state.enemies) do |enemy| | |
| { | |
| x: (enemy.x / WORLD_WIDTH * MINIMAP_SIZE), | |
| y: (enemy.y / WORLD_HEIGHT * MINIMAP_SIZE), | |
| w: 1, | |
| h: 1, | |
| **enemy.color, | |
| primitive_marker: :solid | |
| } | |
| end | |
| end | |
| end | |
| GTK.warn_array_primitives! | |
| def global_keyboard(args) | |
| if args.keyboard.key_up.escape || args.keyboard.key_up.p | |
| args.state.paused = !args.state.paused | |
| end | |
| if args.keyboard.key_up.f | |
| args.state.fx = !args.state.fx | |
| end | |
| if args.inputs.keyboard.key_up.enter | |
| args.state.current_screen = :game | |
| end | |
| if args.inputs.keyboard.key_up.m | |
| args.state.minimap_enabled = !args.state.minimap_enabled | |
| end | |
| end | |
| def tick(args) | |
| if Kernel.tick_count.zero? | |
| args.state.debug = DEBUG | |
| args.state.fx = true | |
| args.state.first_game_tick = false | |
| args.state.minimap_enabled = true | |
| end | |
| $game ||= Game.new(args) | |
| $game.args = args | |
| args.state.current_screen ||= :game | |
| args.state.paused ||= false | |
| args.state.wave ||= 1 | |
| args.state.next_wave_time ||= 0 | |
| args.state.camera_x = $game.instance_variable_get(:@camera)[:x] | |
| args.state.camera_y = $game.instance_variable_get(:@camera)[:y] | |
| if args.state.paused | |
| PauseScreen.draw(args) | |
| elsif args.state.current_screen == :start | |
| StartScreen.draw(args) | |
| elsif args.state.current_screen == :game | |
| $game.tick | |
| end | |
| if Kernel.tick_count.zero? | |
| args.outputs.static_sprites << [ | |
| { | |
| x: 0, | |
| y: 0, | |
| w: SCREEN_WIDTH, | |
| h: SCREEN_HEIGHT, | |
| path: "assets/v-crush.png", | |
| a: 255 | |
| }, | |
| { | |
| x: 0, | |
| y: 0, | |
| w: SCREEN_WIDTH, | |
| h: SCREEN_HEIGHT, | |
| path: "assets/v-crush.png", | |
| a: 255 | |
| } | |
| ] | |
| args.render_target(:brightness).primitives << [ | |
| { | |
| x: 0, | |
| y: 0, | |
| w: SCREEN_WIDTH, | |
| h: SCREEN_HEIGHT, | |
| r: 130 - 50, | |
| g: 130 - 50, | |
| b: 160 - 0, | |
| blendmode_enum: 4, | |
| primitive_marker: :solid | |
| } | |
| ] | |
| if args.state.tick_count.zero? && args.state.fx | |
| args.outputs.static_sprites << [ | |
| { | |
| x: 0, | |
| y: 0, | |
| w: SCREEN_WIDTH, | |
| h: SCREEN_HEIGHT, | |
| path: "assets/scanline.png", | |
| a: 10 | |
| }, | |
| { | |
| x: 0, | |
| y: 0, | |
| w: SCREEN_WIDTH, | |
| h: SCREEN_HEIGHT, | |
| path: :brightness, | |
| a: 255, | |
| blendmode_enum: 4 | |
| } | |
| ] | |
| end | |
| end | |
| Utils.render_debug(args) | |
| global_keyboard(args) | |
| end | |
| $gtk.reset |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment