445 lines
12 KiB
Ruby
445 lines
12 KiB
Ruby
require 'gosu'
|
|
require 'securerandom'
|
|
|
|
puts "Hexagonal Terminal Emulator"
|
|
puts "---------------------------"
|
|
puts "Gosu Version: #{Gosu::VERSION}"
|
|
puts "Ruby Version: #{RUBY_VERSION}"
|
|
|
|
$fg_color = Gosu::Color.new(0, 255, 0)
|
|
$bg_color = Gosu::Color.new(0, 0, 0)
|
|
$hex_color = Gosu::Color.new(255, 255, 255)
|
|
$pallette16 = [
|
|
Gosu::Color.new( 0, 0, 0), # Black 000000
|
|
Gosu::Color.new(255, 0, 0), # Red FF0000
|
|
Gosu::Color.new( 0, 255, 0), # Green 00FF00
|
|
Gosu::Color.new(255, 255, 0), # Yellow FFFF00
|
|
Gosu::Color.new( 0, 0, 255), # Blue 0000FF
|
|
Gosu::Color.new(255, 0, 255), # Magenta FF00FF
|
|
Gosu::Color.new( 0, 255, 255), # Cyan 00FFFF
|
|
Gosu::Color.new(128, 128, 128), # White 808080
|
|
Gosu::Color.new( 64, 64, 64), # Bright Black 404040
|
|
Gosu::Color.new(255, 128, 128), # Bright Red 008080
|
|
Gosu::Color.new(128, 255, 128), # Bright Green 800080
|
|
Gosu::Color.new(255, 255, 128), # Bright Yellow 000080
|
|
Gosu::Color.new(128, 128, 255), # Bright Blue 808000
|
|
Gosu::Color.new(255, 128, 255), # Bright Magenta 008000
|
|
Gosu::Color.new(128, 255, 255), # Bright Cyan 800000
|
|
Gosu::Color.new(255, 255, 255), # Bright White FFFFFF
|
|
]
|
|
#WHITE = Gosu::Color.new(255, 255, 255) # For image drawing purposes
|
|
|
|
$hex_width = 20
|
|
$hex_mid = 40
|
|
$hex_tip = 5
|
|
|
|
HEX_DEBUG_WIDTH = 15
|
|
def recalculate_hexagons
|
|
# 0
|
|
# / \
|
|
# 5 1
|
|
# | |
|
|
# 4 2
|
|
# \ /
|
|
# 3
|
|
$hex_points = [
|
|
# A six pointed hexagon
|
|
[$hex_width/2, 0 ], # Top center
|
|
[$hex_width , $hex_tip ], # Upper right
|
|
[$hex_width , $hex_tip+$hex_mid ], # Lower right
|
|
[$hex_width/2, ($hex_tip*2)+$hex_mid], # Bottom center
|
|
[0 , $hex_tip+$hex_mid ], # Lower left
|
|
[0 , $hex_tip ], # Upper left
|
|
# Iterate in a clockwise order
|
|
]
|
|
puts "Recalculated hexagons:"
|
|
puts "(#{$hex_points[0][0]},#{$hex_points[0][1]})".center(HEX_DEBUG_WIDTH)
|
|
puts "(#{$hex_points[5][0]},#{$hex_points[5][1]})".ljust(HEX_DEBUG_WIDTH/2) \
|
|
+ "(#{$hex_points[1][0]},#{$hex_points[1][1]})".rjust(HEX_DEBUG_WIDTH/2)
|
|
puts "(#{$hex_points[4][0]},#{$hex_points[4][1]})".ljust(HEX_DEBUG_WIDTH/2) \
|
|
+ "(#{$hex_points[2][0]},#{$hex_points[2][1]})".rjust(HEX_DEBUG_WIDTH/2)
|
|
puts "(#{$hex_points[3][0]},#{$hex_points[3][1]})".center(HEX_DEBUG_WIDTH)
|
|
$hex_x_offset = $hex_width/2
|
|
$hex_y_offset = $hex_tip + $hex_mid
|
|
$hex_bgs.clear!
|
|
end
|
|
|
|
class HexEditXref_BgGrphStore
|
|
def initialize
|
|
clear!
|
|
end
|
|
def clear!
|
|
@hex_ids = Hash.new #hexid => color
|
|
@hex_refs = Hash.new #color => hexids
|
|
@hex_bgs = Hash.new #color => bg
|
|
end
|
|
def register(hexid,color)
|
|
raise TypeError, ("color must be a Gosu::Color, not a '#{color.class}'"
|
|
) unless color.is_a? Gosu::Color
|
|
argb = color.argb
|
|
return if (@hex_ids.has_key?(hexid) &&
|
|
@hex_ids[hexid] == argb)
|
|
unregister hexid
|
|
@hex_ids[hexid] = argb
|
|
if @hex_refs.has_key? argb
|
|
@hex_refs[argb].add(hexid)
|
|
else
|
|
@hex_refs[argb] = Set.new([hexid, ])
|
|
end
|
|
render argb
|
|
end
|
|
def unregister(hexid)
|
|
return unless @hex_ids.has_key? hexid
|
|
# ➣ If the key doesn't exist, assume
|
|
# the call was made mistakenly.
|
|
# Note: may cause leaks later
|
|
|
|
argb = @hex_ids[hexid]
|
|
@hex_ids.delete hexid
|
|
if @hex_refs.has_key? argb
|
|
@hex_refs[argb].delete hexid
|
|
if @hex_refs[argb].size < 1
|
|
@hex_refs.delete argb
|
|
end
|
|
end
|
|
if (@hex_bgs.has_key?(argb) &&
|
|
!@hex_refs.has_key?(argb))
|
|
@hex_bgs.delete argb
|
|
end
|
|
end
|
|
def [](color)
|
|
raise TypeError, ("color must be a Gosu::Color, not a '#{color.class}'"
|
|
) unless color.is_a? Gosu::Color
|
|
argb = color.argb
|
|
raise RuntimeError.new(
|
|
"Color #{argb} was requested from "+
|
|
"a HEX_BGs class, but was not "+
|
|
"registered beforehand."
|
|
) unless @hex_bgs.has_key? argb
|
|
return @hex_bgs[argb]
|
|
end
|
|
# Debug methods
|
|
def debug_get_refs(color)
|
|
raise TypeError, ("color must be a Gosu::Color, not a '#{color.class}'"
|
|
) unless color.is_a? Gosu::Color
|
|
argb = color.argb
|
|
return nil unless @hex_refs.has_key? argb
|
|
return Set.new(@hex_refs[argb])
|
|
end
|
|
def debug_get_argb_for_id(hexid)
|
|
return nil unless @hex_ids.has_key? hexid
|
|
return @hex_ids[hexid]
|
|
end
|
|
def debug_get_bg_for_id(hexid)
|
|
return nil unless @hex_ids.has_key? hexid
|
|
argb = @hex_ids[hexid]
|
|
return nil unless @hex_bgs.has_key? argb
|
|
return @hex_bgs[argb]
|
|
end
|
|
def debug_get_bg_for_color(color)
|
|
raise TypeError, ("color must be a Gosu::Color, not a '#{color.class}'"
|
|
) unless color.is_a? Gosu::Color
|
|
argb = color.argb
|
|
return nil unless @hex_bgs.has_key? argb
|
|
return @hex_bgs[argb]
|
|
end
|
|
def debug_get_stats
|
|
return {
|
|
'b' => @hex_bgs.size,
|
|
'i' => @hex_ids.size,
|
|
'r' => @hex_refs.size,
|
|
}
|
|
end
|
|
private
|
|
def render(argb)
|
|
color = Gosu::Color.new(argb)
|
|
@hex_bgs[argb] = Gosu::record($hex_width, $hex_mid+($hex_tip*2)) {
|
|
# Double Quad Method (0145, 1234):
|
|
Gosu::draw_quad( # 0145
|
|
$hex_points[0][0], $hex_points[0][1], color, #0
|
|
$hex_points[1][0], $hex_points[1][1], color, #1
|
|
$hex_points[4][0], $hex_points[4][1], color, #4
|
|
$hex_points[5][0], $hex_points[5][1], color, #5
|
|
0
|
|
)
|
|
Gosu.draw_quad( # 1234
|
|
$hex_points[1][0], $hex_points[1][1], color, #1
|
|
$hex_points[2][0], $hex_points[2][1], color, #2
|
|
$hex_points[3][0], $hex_points[3][1], color, #3
|
|
$hex_points[4][0], $hex_points[4][1], color, #4
|
|
0
|
|
)
|
|
}
|
|
end
|
|
end
|
|
|
|
class TextProps
|
|
attr_accessor \
|
|
:font, :bg_color, :fg_color,
|
|
:hex_border_colors,
|
|
:bold, :italic, :underline, :overline, :strikethrough
|
|
def initialize(font, text_props=nil)
|
|
if text_props != nil
|
|
@font = font
|
|
@fg_color = text_props.fg_color
|
|
@bg_color = text_props.bg_color
|
|
@hex_border_colors = text_props.hex_border_colors
|
|
@bold = text_props.bold
|
|
@italic = text_props.italic
|
|
@underline = text_props.underline
|
|
@overline = text_props.overline
|
|
@strikethrough = text_props.strikethrough
|
|
return
|
|
end
|
|
@font = font
|
|
@fg_color = $fg_color
|
|
@bg_color = $bg_color
|
|
@hex_border_colors = [
|
|
$hex_color, $hex_color,
|
|
$hex_color, $hex_color,
|
|
$hex_color, $hex_color]
|
|
@bold = false
|
|
@italic = false
|
|
@underline = false
|
|
@overline = false
|
|
@strikethrough = false
|
|
return
|
|
end
|
|
end
|
|
|
|
class Hex
|
|
attr_accessor :x, :y, :char, :text_props
|
|
def initialize(x, y, char, text_props)
|
|
@x = x
|
|
@y = y
|
|
@char = char
|
|
@text_props = text_props
|
|
@hexid = SecureRandom.uuid
|
|
@dead = false
|
|
end
|
|
def update
|
|
amidead?
|
|
$hex_bgs.register(@hexid, @text_props.bg_color)
|
|
end
|
|
def draw
|
|
amidead?
|
|
$hex_bgs[@text_props.bg_color].draw(
|
|
@x, @y, 0, 1, 1, Gosu::Color::WHITE
|
|
) # layer 0: hex-bg
|
|
for i1 in 0..5
|
|
i2 = (i1+1)%6
|
|
x1=$hex_points[i1][0]+@x; y1=$hex_points[i1][1]+@y
|
|
x2=$hex_points[i2][0]+@x; y2=$hex_points[i2][1]+@y
|
|
Gosu::draw_line(
|
|
x1, y1, @text_props.hex_border_colors[i1],
|
|
x2, y2, @text_props.hex_border_colors[i1],
|
|
1
|
|
) # layer 1: hex-border
|
|
end
|
|
@text_props.font.draw_text(
|
|
@char, @x, @y+$hex_tip,
|
|
2, 1.0, 1.0, @text_props.fg_color
|
|
) # layer 2: hex-text
|
|
end
|
|
|
|
def cleanup!
|
|
$hex_bgs.unregister @hexid
|
|
@dead = true
|
|
end
|
|
|
|
private
|
|
def amidead?
|
|
if @dead
|
|
raise StandardError, (
|
|
"Hexagon #{@hexid} at (#{@x}, #{@y}) has been "+
|
|
"cleaned up, and should not have been called '"+
|
|
"draw' on."
|
|
)
|
|
return true # technically unreachable, but here for clarity
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
|
|
class HexagonTerminalWindow < Gosu::Window
|
|
def initialize(width, height, fullscreen)
|
|
# Window
|
|
super
|
|
self.caption = "Hexagon Terminal"
|
|
@width = width
|
|
@height = height
|
|
|
|
# Fonts (load default monospaced font)
|
|
@font = Gosu::Font.new(self, "Monaspace Krypton", 30)
|
|
$hex_width = @font.text_width("_").round()
|
|
$hex_mid = @font.height.round()
|
|
recalculate_hexagons
|
|
|
|
# State
|
|
@cursor_pos = [0, 1] # row-1, col-1
|
|
@input_buffer = ""
|
|
@output_buffer = "."
|
|
@active_styling = TextProps.new(@font)
|
|
@do_redraw = true
|
|
self.text_input = Gosu::TextInput.new
|
|
@old_text = [0,'',0]
|
|
|
|
# Hexes
|
|
@xexes = ((width-$hex_x_offset) / $hex_width).floor # xexes = 'x's of hexes
|
|
@yexes = ((height-$hex_tip) / $hex_y_offset ).floor # yexes = 'y's of hexes
|
|
@next_row_is_shifted = false
|
|
@hexes = []
|
|
for row in 0..@yexes-1
|
|
@hexes << []
|
|
for col in 0..@xexes-1
|
|
x = (col*$hex_width) + ((row % 2 == 1) ? $hex_x_offset : 0)
|
|
y = row*$hex_y_offset
|
|
@active_styling.bg_color = Gosu::Color.new(rand(255), rand(255), rand(255))
|
|
@hexes[-1] << Hex.new(x, y, "W", TextProps.new(@font, @active_styling))
|
|
@next_row_is_shifted = row % 2 == 0
|
|
end
|
|
end
|
|
|
|
# Clear the screen
|
|
@hexes.each do |rex|
|
|
rex.each do |hex|
|
|
hex.char = ' '
|
|
end
|
|
end
|
|
end
|
|
|
|
def needs_redraw?
|
|
return @do_redraw
|
|
end
|
|
|
|
def update
|
|
hbgdb = $hex_bgs.debug_get_stats
|
|
# puts "ids: #{hbgdb['i']}"
|
|
# puts "ref: #{hbgdb['r']}"
|
|
# puts "bgs: #{hbgdb['b']}"
|
|
|
|
if @output_buffer.length > 0
|
|
render_output
|
|
@do_redraw = true
|
|
end
|
|
new_text = [
|
|
self.text_input.selection_start,
|
|
self.text_input.text,
|
|
self.text_input.caret_pos
|
|
]
|
|
if @old_text != new_text
|
|
@old_text = [
|
|
new_text[0],
|
|
new_text[1],
|
|
new_text[2]
|
|
]
|
|
out = ''+new_text[1]
|
|
if new_text[0]<new_text[2]
|
|
out.insert(new_text[2],"\x1b[m|")
|
|
out.insert(new_text[0],"\x1b[7m")
|
|
elsif new_text[2]<new_text[0]
|
|
out.insert(new_text[0],"\x1b[m")
|
|
out.insert(new_text[2],"|\x1b[7m")
|
|
else
|
|
out.insert(new_text[2],"|")
|
|
end
|
|
print "\x1b[G\x1b[2K#{out}\x1b[#{new_text[2]+1}G"
|
|
end
|
|
|
|
# End of update should call hex updates
|
|
@hexes.each do |rex|
|
|
rex.each do |hex|
|
|
hex.update
|
|
end
|
|
end
|
|
end
|
|
|
|
def draw
|
|
@do_redraw = false
|
|
# Draw the background (clears the screen)
|
|
Gosu::draw_rect(
|
|
0, 0, @width, @height, $bg_color, 0
|
|
)
|
|
# Draw the hexes
|
|
@hexes.each do |rex| # rex=row of hexes
|
|
rex.each do |hex|
|
|
hex.draw
|
|
end
|
|
end
|
|
end
|
|
|
|
def render_output
|
|
# Render things from the output buffer
|
|
# into the hex grid
|
|
|
|
@output_buffer.each_char.with_index do |char, i|
|
|
if @escape_mode
|
|
handle_escape_char(char)
|
|
end
|
|
case char
|
|
when "\n"
|
|
@cursor_pos[0] += 1
|
|
@cursor_pos[1] = 0
|
|
when "\x1b" # ESC
|
|
@escape_mode = true
|
|
@escape_buffer = ""
|
|
next
|
|
else
|
|
@hexes[@cursor_pos[0]][@cursor_pos[1]].char = char
|
|
@cursor_pos[1] += 1
|
|
end
|
|
|
|
if @cursor_pos[1] >= @hexes[0].length
|
|
@cursor_pos[0] += 1
|
|
@cursor_pos[1] = 0
|
|
end
|
|
if @cursor_pos[0] >= @hexes.length
|
|
@cursor_pos[0] = @hexes.length - 1
|
|
scroll_with_new_line
|
|
end
|
|
end
|
|
@do_redraw = true
|
|
# @output_buffer = ""
|
|
end
|
|
|
|
def scroll_with_new_line
|
|
@active_styling.hex_border_colors = [
|
|
Gosu::Color::WHITE,
|
|
Gosu::Color::WHITE,
|
|
Gosu::Color::WHITE,
|
|
Gosu::Color::WHITE,
|
|
Gosu::Color::WHITE,
|
|
Gosu::Color::WHITE
|
|
]
|
|
@hexes.each do |rex|
|
|
rex.each do |hex|
|
|
hex.y -= $hex_y_offset
|
|
end
|
|
end
|
|
@hexes[0].each.with_index do |hex, i|
|
|
hex.cleanup!
|
|
@hexes[0][i] = nil
|
|
end
|
|
@hexes = @hexes[1..-1]
|
|
@hexes << []
|
|
for i in 0..@xexes-1
|
|
@active_styling.bg_color = Gosu::Color::BLACK
|
|
@hexes[-1] << Hex.new(
|
|
(i*$hex_width) + (@next_row_is_shifted? $hex_x_offset : 0),
|
|
(@yexes-1)*$hex_y_offset,
|
|
" ", TextProps.new(@font, @active_styling))
|
|
end
|
|
@next_row_is_shifted = !@next_row_is_shifted
|
|
end
|
|
|
|
def handle_escape_char(char)
|
|
@escape_buffer += @output_buffer[0]
|
|
@output_buffer = @output_buffer[1..-1]
|
|
end
|
|
end
|
|
|
|
$hex_bgs = HexEditXref_BgGrphStore.new
|
|
recalculate_hexagons
|
|
window = HexagonTerminalWindow.new(800, 600, false)
|
|
window.show
|