beefdump 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +13 -0
- data/lib/beefdump.rb +16 -0
- data/lib/beefdump/boot.rb +35 -0
- data/lib/beefdump/client/base.rb +33 -0
- data/lib/beefdump/client/graphical/client.rb +94 -0
- data/lib/beefdump/client/graphical/window.rb +24 -0
- data/lib/beefdump/config/base.rb +8 -0
- data/lib/beefdump/config/blueprint.rb +7 -0
- data/lib/beefdump/game/base.rb +95 -0
- data/lib/beefdump/game/player.rb +12 -0
- data/lib/beefdump/game/state.rb +22 -0
- data/lib/beefdump/logger.rb +19 -0
- data/lib/beefdump/map/layer.rb +53 -0
- data/lib/beefdump/map/map.rb +136 -0
- data/lib/beefdump/map/object.rb +75 -0
- data/lib/beefdump/map/tileset.rb +35 -0
- data/lib/beefdump/monkey_patches/open_struct.rb +4 -0
- data/lib/beefdump/monkey_patches/string.rb +5 -0
- data/lib/beefdump/utils/image_util.rb +47 -0
- data/lib/beefdump/world/entity.rb +6 -0
- data/lib/beefdump/world/object.rb +37 -0
- data/lib/beefdump/world/object_property.rb +24 -0
- data/lib/beefdump/world/player.rb +6 -0
- data/lib/beefdump/world/world.rb +25 -0
- metadata +103 -0
data/README.md
ADDED
data/lib/beefdump.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Beefdump
|
2
|
+
# Bootstrap helpers
|
3
|
+
def self.load_dir(dir)
|
4
|
+
Dir.new(dir).each do |file|
|
5
|
+
next unless file =~ /\.rb$/
|
6
|
+
load "#{dir}/#{file}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Load path
|
11
|
+
LIB_PATH = File.expand_path(File.dirname(__FILE__))
|
12
|
+
$LOAD_PATH << LIB_PATH
|
13
|
+
|
14
|
+
# Monkey patches
|
15
|
+
load_dir("#{LIB_PATH}/beefdump/monkey_patches")
|
16
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Beefdump
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
# Config
|
5
|
+
require 'beefdump/config/base'
|
6
|
+
CONFIG_PATH = "#{ROOT_PATH}/config"
|
7
|
+
CONFIG = Config::Base.new
|
8
|
+
# load beefdump config blueprint
|
9
|
+
require 'beefdump/config/blueprint'
|
10
|
+
|
11
|
+
base_config = "#{CONFIG_PATH}/base.rb"
|
12
|
+
load base_config if File.exist?(base_config)
|
13
|
+
|
14
|
+
# Logger
|
15
|
+
load CONFIG.logger.class
|
16
|
+
Logger.level = CONFIG.logger.level
|
17
|
+
Logger.info "Monkey patches, config and logger loaded. Let's get going!"
|
18
|
+
|
19
|
+
# Utils
|
20
|
+
UTILS_PATH = "#{LIB_PATH}/beefdump/utils"
|
21
|
+
Logger.info "Loading utils."
|
22
|
+
load_dir(UTILS_PATH)
|
23
|
+
|
24
|
+
# Game
|
25
|
+
Logger.info "Loading game kernel"
|
26
|
+
load "#{LIB_PATH}/beefdump/game/base.rb"
|
27
|
+
|
28
|
+
# Modules
|
29
|
+
Logger.info "Loading beefdump modules"
|
30
|
+
CONFIG.active_modules.each do |modul|
|
31
|
+
Logger.trace "Loading module #{modul}"
|
32
|
+
require "beefdump/#{modul}#{modul =~ /.+\/.+/ ? "" : "/#{modul}"}"
|
33
|
+
end
|
34
|
+
Logger.info "Loading of modules completed"
|
35
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Client
|
3
|
+
# The most basic client which is kind of a client specification
|
4
|
+
class Base
|
5
|
+
attr_reader :player
|
6
|
+
|
7
|
+
# Initialize the client on connection to the server
|
8
|
+
def initialize(game, player = nil)
|
9
|
+
@game = game
|
10
|
+
@player = player
|
11
|
+
end
|
12
|
+
|
13
|
+
# Do whatever the client would like to do.
|
14
|
+
# This is called by the server in any game loop.
|
15
|
+
def act
|
16
|
+
end
|
17
|
+
|
18
|
+
# The server wants to close the connection to the client.
|
19
|
+
# Clean up the stuff in here
|
20
|
+
def disconnect
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
def id
|
25
|
+
object_id
|
26
|
+
end
|
27
|
+
|
28
|
+
def log(level, message)
|
29
|
+
Logger.send(level, "Client #{id}: #{message}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Client
|
3
|
+
module Graphical
|
4
|
+
class Client < Client::Base
|
5
|
+
require 'gosu'
|
6
|
+
require 'beefdump/client/graphical/window'
|
7
|
+
|
8
|
+
def initialize(game, *options)
|
9
|
+
super(game, *options)
|
10
|
+
@window = Window.new(self, Window::WIDTH, Window::HEIGHT, false)
|
11
|
+
@window.caption = Time.now
|
12
|
+
|
13
|
+
initialize_tilesets(game.map)
|
14
|
+
|
15
|
+
@player = @game.claim_player("Peter", self)
|
16
|
+
start_main_loop
|
17
|
+
end
|
18
|
+
|
19
|
+
def act
|
20
|
+
@window.caption = Time.now
|
21
|
+
end
|
22
|
+
|
23
|
+
def disconnect
|
24
|
+
@window.close
|
25
|
+
@window_thread.join
|
26
|
+
end
|
27
|
+
|
28
|
+
def update_window
|
29
|
+
return unless @game.should_run?
|
30
|
+
|
31
|
+
if @window.button_down? Gosu::Button::KbEscape
|
32
|
+
@game.shutdown!
|
33
|
+
end
|
34
|
+
handle_camera_movement
|
35
|
+
end
|
36
|
+
|
37
|
+
def draw_window
|
38
|
+
left = @player.position.x / @game.map.tile_width
|
39
|
+
top = @player.position.y / @game.map.tile_height
|
40
|
+
diff_x = @player.position.x % @game.map.tile_width
|
41
|
+
diff_y = @player.position.y % @game.map.tile_height
|
42
|
+
width = Window::WIDTH / @game.map.tile_width + 1
|
43
|
+
height = Window::HEIGHT / @game.map.tile_height + 1
|
44
|
+
|
45
|
+
tile_layers = @game.map.background_at(left, top, width, height)
|
46
|
+
|
47
|
+
tile_layers.each do |tile_layer|
|
48
|
+
tile_layer.each_with_index do |row, x|
|
49
|
+
row.each_with_index do |tile, y|
|
50
|
+
tileset = @game.map.tileset_for(tile)
|
51
|
+
next unless tileset
|
52
|
+
tile_id = tile - tileset.first_gid
|
53
|
+
@tileset_images[@game.map.tilesets.index(tileset)][tile_id].draw(x * @game.map.tile_width - diff_x, y * @game.map.tile_height - diff_y, 0)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
def start_main_loop
|
61
|
+
Thread.abort_on_exception = true
|
62
|
+
@window_thread = Thread.new do
|
63
|
+
@window.show
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize_tilesets(map)
|
68
|
+
@tileset_images = []
|
69
|
+
map.tilesets.each do |tileset|
|
70
|
+
log(:trace, "Loading tileset image '#{tileset.image_file}'")
|
71
|
+
@tileset_images << Gosu::Image.load_tiles(@window, tileset.image_file, tileset.tile_width, tileset.tile_height, true)
|
72
|
+
log(:trace, "Loaded #{@tileset_images.last.size} tiles.")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_camera_movement
|
77
|
+
if @window.button_down? Gosu::Button::KbRight
|
78
|
+
@player.position.x = [(@game.map.width * @game.map.tile_width) - Window::WIDTH, @player.position.x + 5].min
|
79
|
+
end
|
80
|
+
if @window.button_down? Gosu::Button::KbLeft
|
81
|
+
@player.position.x = [0, @player.position.x - 5].max
|
82
|
+
end
|
83
|
+
|
84
|
+
if @window.button_down? Gosu::Button::KbDown
|
85
|
+
@player.position.y = [(@game.map.height * @game.map.tile_height) - Window::HEIGHT, @player.position.y + 5].min
|
86
|
+
end
|
87
|
+
if @window.button_down? Gosu::Button::KbUp
|
88
|
+
@player.position.y = [0, @player.position.y - 5].max
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Client
|
3
|
+
module Graphical
|
4
|
+
require 'gosu'
|
5
|
+
class Window < Gosu::Window
|
6
|
+
WIDTH = 640
|
7
|
+
HEIGHT = 480
|
8
|
+
|
9
|
+
def initialize(client, *args)
|
10
|
+
super(*args)
|
11
|
+
@client = client
|
12
|
+
end
|
13
|
+
|
14
|
+
def update
|
15
|
+
@client.update_window
|
16
|
+
end
|
17
|
+
|
18
|
+
def draw
|
19
|
+
@client.draw_window
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Game
|
3
|
+
class Base
|
4
|
+
require 'beefdump/game/state'
|
5
|
+
require 'beefdump/game/player'
|
6
|
+
|
7
|
+
CLIENT_UPDATE_RATE = 1.0 / 10 # 10 times a second
|
8
|
+
GAME_STATE_PATH = "#{ROOT_PATH}/game_state"
|
9
|
+
CURRENT_GAME_STATE_FILE = "#{GAME_STATE_PATH}/current.bin"
|
10
|
+
|
11
|
+
attr_reader :map
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
load_static_data
|
15
|
+
load_map('test')
|
16
|
+
init_game_state
|
17
|
+
|
18
|
+
load_clients
|
19
|
+
|
20
|
+
@shutdown = false
|
21
|
+
|
22
|
+
game_loop
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_map(new_map)
|
26
|
+
@map = Map.load(new_map)
|
27
|
+
end
|
28
|
+
|
29
|
+
def shutdown!
|
30
|
+
Logger.info "Received shutdown command from client. Quitting game."
|
31
|
+
save_game_state
|
32
|
+
@shutdown = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def claim_player(name, client)
|
36
|
+
player = @state.players.select {|p| p.name == name}.first
|
37
|
+
raise "Player not found" unless player
|
38
|
+
player
|
39
|
+
end
|
40
|
+
|
41
|
+
def should_run?
|
42
|
+
!@shutdown
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def init_game_state
|
48
|
+
Logger.info("Loading game state.")
|
49
|
+
|
50
|
+
if File.exist?(CURRENT_GAME_STATE_FILE)
|
51
|
+
@state = File.open(CURRENT_GAME_STATE_FILE, 'r') {|f| Marshal.load(f)}
|
52
|
+
else
|
53
|
+
Logger.info("No saved game state found.")
|
54
|
+
initially_create_game_state!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def initially_create_game_state!
|
59
|
+
initial_state = @map.get_initial_state
|
60
|
+
@state = Game::State.from_xml(initial_state, self)
|
61
|
+
end
|
62
|
+
|
63
|
+
def save_game_state
|
64
|
+
Logger.info("Saving game state")
|
65
|
+
File.open(CURRENT_GAME_STATE_FILE, 'w') {|f| Marshal.dump(@state, f)}
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_static_data
|
69
|
+
World.load
|
70
|
+
end
|
71
|
+
|
72
|
+
def load_clients
|
73
|
+
Logger.info "Loading clients."
|
74
|
+
@clients = []
|
75
|
+
@clients << Client::Graphical::Client.new(self)
|
76
|
+
end
|
77
|
+
|
78
|
+
def game_loop
|
79
|
+
Logger.info "All systems go! Starting main game loop."
|
80
|
+
|
81
|
+
while self.should_run?
|
82
|
+
start_time = Time.now.to_f
|
83
|
+
|
84
|
+
@clients.each do |client|
|
85
|
+
client.act
|
86
|
+
end
|
87
|
+
|
88
|
+
passed_time = Time.now.to_f - start_time
|
89
|
+
sleep passed_time > CLIENT_UPDATE_RATE ? 0 : CLIENT_UPDATE_RATE - passed_time
|
90
|
+
end
|
91
|
+
Logger.info("fin")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Game
|
3
|
+
class State
|
4
|
+
require 'ostruct'
|
5
|
+
attr_reader :players, :game
|
6
|
+
|
7
|
+
def initialize(players)
|
8
|
+
@players = players
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.from_xml(xml, game)
|
12
|
+
players = []
|
13
|
+
xml['players'].first['player'].each do |player|
|
14
|
+
position = player['position'].first
|
15
|
+
players << Game::Player.new(player['name'], OpenStruct.new({:x => position['x'].to_i, :y => position['y'].to_i}))
|
16
|
+
end
|
17
|
+
|
18
|
+
self.new(players)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Beefdump
|
2
|
+
class Logger
|
3
|
+
LEVELS = [:trace, :info, :warn, :error]
|
4
|
+
|
5
|
+
def self.level=(new_level)
|
6
|
+
raise "No valid log level: '#{new_level}'!" unless @level = LEVELS.index(new_level)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.method_missing(method_name, *args)
|
10
|
+
return super.send(method_name, args) unless LEVELS.include?(method_name)
|
11
|
+
log(method_name, args.first) if LEVELS.index(method_name) >= @level
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
def self.log(level, message)
|
16
|
+
puts "[#{level}] #{Time.now}: #{message}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Map
|
3
|
+
class Layer
|
4
|
+
require 'zlib'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
attr_reader :name, :width, :height, :map, :field
|
8
|
+
|
9
|
+
def initialize(layer_data, map)
|
10
|
+
@map = map
|
11
|
+
|
12
|
+
load_layer_attributes!(layer_data)
|
13
|
+
|
14
|
+
load_data!(layer_data["data"].first)
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def load_layer_attributes!(layer_data)
|
19
|
+
@name = layer_data["name"]
|
20
|
+
@width = layer_data["width"].to_i
|
21
|
+
@height = layer_data["height"].to_i
|
22
|
+
|
23
|
+
Logger.trace "Loaded layer attributes: Name: #{@name} Width: #{@width} Height: #{@height}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_data!(data)
|
27
|
+
Logger.trace "Loading layer data for layer '#{@name}'."
|
28
|
+
|
29
|
+
encoding = data["encoding"]
|
30
|
+
compression = data["compression"]
|
31
|
+
|
32
|
+
raise "Map layers without BASE64 enconding are unsupported!" unless encoding == "base64"
|
33
|
+
raise "Map layers without GZIP compression are unsupported!" unless compression == "gzip"
|
34
|
+
|
35
|
+
content = StringIO.new(Base64.decode64(data["content"].strip))
|
36
|
+
gz = Zlib::GzipReader.new(content)
|
37
|
+
|
38
|
+
@field = []
|
39
|
+
row = []
|
40
|
+
byte_arr = []
|
41
|
+
gz.each_byte do |byte|
|
42
|
+
byte_arr << byte
|
43
|
+
if byte_arr.size == 4
|
44
|
+
row << (byte_arr[0] | byte_arr[1] << 8 | byte_arr[2] << 16 | byte_arr[3] << 24)
|
45
|
+
byte_arr = []
|
46
|
+
@field << row and row = [] if row.size == @width
|
47
|
+
end
|
48
|
+
end
|
49
|
+
gz.close
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Map
|
3
|
+
MAP_PATH = "#{ROOT_PATH}/map"
|
4
|
+
|
5
|
+
class Base
|
6
|
+
require 'xmlsimple'
|
7
|
+
require 'beefdump/map/layer'
|
8
|
+
require 'beefdump/map/tileset'
|
9
|
+
require 'beefdump/map/object'
|
10
|
+
|
11
|
+
MAP_FORMAT_VERSION = "1.0"
|
12
|
+
|
13
|
+
attr_reader :width, :height, :tile_width, :tile_height, :layers, :tilesets, :objects, :name
|
14
|
+
|
15
|
+
def initialize(map_name, map_data)
|
16
|
+
@name = map_name
|
17
|
+
|
18
|
+
check_compatibility!(map_data)
|
19
|
+
|
20
|
+
load_map_attributes!(map_data)
|
21
|
+
load_layers!(map_data)
|
22
|
+
load_tilesets!(map_data)
|
23
|
+
load_objects!(map_data)
|
24
|
+
|
25
|
+
@raw_data = map_data
|
26
|
+
end
|
27
|
+
|
28
|
+
def background_at(x, y, width = 1, height = 1)
|
29
|
+
tiles = []
|
30
|
+
@layers.each do |layer|
|
31
|
+
field = Array.new(width).map {|e| Array.new(height).map {|e| nil}}
|
32
|
+
for diff_x in 0..(width - 1)
|
33
|
+
for diff_y in 0..(height - 1)
|
34
|
+
field[diff_x][diff_y] = layer.field[y + diff_y] ? layer.field[y + diff_y][x + diff_x] : nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
tiles << field
|
38
|
+
end
|
39
|
+
|
40
|
+
tiles
|
41
|
+
end
|
42
|
+
|
43
|
+
def objects_at(x, y)
|
44
|
+
objects = @objects.select {|o| o.is_at?(x, y)}
|
45
|
+
end
|
46
|
+
|
47
|
+
def tileset_for(gid)
|
48
|
+
return unless gid
|
49
|
+
chosen = nil
|
50
|
+
@tilesets.each do |tileset|
|
51
|
+
break if chosen && gid < tileset.first_gid
|
52
|
+
chosen = tileset if gid >= tileset.first_gid
|
53
|
+
end
|
54
|
+
|
55
|
+
chosen
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_initial_state
|
59
|
+
Logger.info("Loading initial state for map '#{@name}'.")
|
60
|
+
initial_state_file = "#{MAP_PATH}/#{@name}.isx"
|
61
|
+
raise "Initial state file for map '#{@name}' not found!" unless File.exist?(initial_state_file)
|
62
|
+
|
63
|
+
XmlSimple.xml_in(initial_state_file)
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
def check_compatibility!(map_data)
|
68
|
+
Logger.trace "Checking map format compatibility."
|
69
|
+
|
70
|
+
raise "Map format unsupported (#{map_data["orientation"]})!" unless map_data["orientation"] == "orthogonal"
|
71
|
+
Logger.warn("Map format version #{map_data['version']} is not supported - scary problems may occur! Supply maps in format version #{MAP_FORMAT_VERSION} to ensure smooth operation.") if map_data['version'] != MAP_FORMAT_VERSION
|
72
|
+
end
|
73
|
+
|
74
|
+
def load_map_attributes!(map_data)
|
75
|
+
@width = map_data["width"].to_i
|
76
|
+
@height = map_data["height"].to_i
|
77
|
+
@tile_width = map_data["tilewidth"].to_i
|
78
|
+
@tile_height = map_data["tileheight"].to_i
|
79
|
+
|
80
|
+
Logger.trace "Loaded map attributes: Width: #{@width} Height: #{@height} Tile Width: #{@tile_width} Tile Height: #{@tile_height}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def load_layers!(map_data)
|
84
|
+
@layers = map_data["layer"].map {|layer_data| Layer.new(layer_data, self)}
|
85
|
+
|
86
|
+
Logger.info "Loaded #{@layers.size} map layers."
|
87
|
+
end
|
88
|
+
|
89
|
+
def load_tilesets!(map_data)
|
90
|
+
@tilesets = map_data["tileset"].map {|tileset_data| Tileset.new(tileset_data, self)}.sort {|a, b| a.first_gid <=> b.first_gid}
|
91
|
+
|
92
|
+
Logger.info "Loaded #{@tilesets.size} tilesets containing #{@tilesets.inject(0) {|a,t| a += t.tiles_count}} tiles at all."
|
93
|
+
end
|
94
|
+
|
95
|
+
def load_objects!(map_data)
|
96
|
+
@objects = []
|
97
|
+
map_data["objectgroup"].each do |object_group|
|
98
|
+
object_group["object"].each do |object|
|
99
|
+
new_object = Object.new(object)
|
100
|
+
raise "Duplicate object name on map: #{new_object.name}" if @objects.any? {|o| o.name == new_object.name}
|
101
|
+
@objects << new_object
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
check_object_reference_integrity
|
106
|
+
|
107
|
+
Logger.info "Loaded #{@objects.size} objects on the map."
|
108
|
+
end
|
109
|
+
|
110
|
+
def check_object_reference_integrity
|
111
|
+
Logger.info "Checking reference integrity on objects."
|
112
|
+
|
113
|
+
@objects.each do |object|
|
114
|
+
object.properties.each do |property, value|
|
115
|
+
if object.blueprint.properties[property].type == :name_reference
|
116
|
+
raise "Object '#{object.name}' references to an object named '#{value}' as it's '#{property}' value but such an object does not exist!" unless @objects.any? {|o| o.name == value}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.load(map_name)
|
124
|
+
Logger.info "Trying to load map #{map_name}"
|
125
|
+
|
126
|
+
map_file = "#{MAP_PATH}/#{map_name}.tmx"
|
127
|
+
raise "Map does not exist: '#{map_file}'!" unless File.exist?(map_file)
|
128
|
+
|
129
|
+
map = Base.new(map_name, XmlSimple.xml_in(map_file))
|
130
|
+
|
131
|
+
Logger.info "Successfully loaded map."
|
132
|
+
map
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Map
|
3
|
+
class Object
|
4
|
+
attr_reader :name, :x, :y, :width, :height, :type, :properties, :blueprint
|
5
|
+
|
6
|
+
def initialize(object_data)
|
7
|
+
load_object_attributes!(object_data)
|
8
|
+
|
9
|
+
load_properties!(object_data["properties"])
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_at?(x, y)
|
13
|
+
self.x >= x && self.x + self.width <= x && self.y >= y && self.y + self.height <= y
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
def load_object_attributes!(object_data)
|
18
|
+
@type = object_data["type"].to_sym
|
19
|
+
@blueprint = World.objects[@type]
|
20
|
+
raise "Unsupported object type '#{@type}' on map!" unless @blueprint
|
21
|
+
|
22
|
+
@name = object_data["name"]
|
23
|
+
@x = object_data["x"]
|
24
|
+
@y = object_data["y"]
|
25
|
+
@width = object_data["width"]
|
26
|
+
@height = object_data["height"]
|
27
|
+
|
28
|
+
Logger.trace "Loaded object attributes: Name: #{@name} Position: #{@x}, #{@y} Width: #{@width} Height: #{@height} Type: #{@type}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_properties!(properties_data)
|
32
|
+
@properties = {}
|
33
|
+
return unless properties_data
|
34
|
+
|
35
|
+
properties_data.first["property"].each do |property_data|
|
36
|
+
value = check_and_cast_property(property_data["property"])
|
37
|
+
@properties[property_data["name"].to_sym] = property_data["value"]
|
38
|
+
end
|
39
|
+
|
40
|
+
check_for_mandatory_properties
|
41
|
+
|
42
|
+
Logger.trace "Sucessfully loaded properties for object '#{@name}'."
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_and_cast_property(property_data)
|
46
|
+
return unless property_data
|
47
|
+
|
48
|
+
property_name = property_data["name"].to_sym
|
49
|
+
property_blueprint = @blueprint.properties[property_name]
|
50
|
+
raise "Object type '#{@type}' has no '#{property_name}' property (on object '#{@name}')" unless property_blueprint
|
51
|
+
|
52
|
+
value = property_data["value"]
|
53
|
+
return nil unless value
|
54
|
+
|
55
|
+
case property_blueprint.type
|
56
|
+
when :number
|
57
|
+
raise "Value of property '#{property_name}' on object '#{@name}' must be numeric and not #{value}!" unless value.numeric?
|
58
|
+
value.to_f
|
59
|
+
when :string
|
60
|
+
value.to_s
|
61
|
+
when :name_reference
|
62
|
+
value.to_s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def check_for_mandatory_properties
|
67
|
+
@blueprint.properties.each do |type, property|
|
68
|
+
next unless property.mandatory?
|
69
|
+
|
70
|
+
raise "Mandatory property '#{property.name}' not set on object '#{@name}' of type #{@type}!" unless @properties[property.name]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module Map
|
3
|
+
class Tileset
|
4
|
+
attr_reader :name, :first_gid, :tile_width, :tile_height, :image_file, :image_transparency_color, :map, :dimensions, :tiles_count
|
5
|
+
|
6
|
+
def initialize(tileset_data, map)
|
7
|
+
@map = map
|
8
|
+
|
9
|
+
load_tileset_attributes!(tileset_data)
|
10
|
+
load_image!(tileset_data["image"].first)
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def load_tileset_attributes!(tileset_data)
|
16
|
+
@name = tileset_data["name"]
|
17
|
+
@first_gid = tileset_data["firstgid"].to_i
|
18
|
+
@tile_width = tileset_data["tilewidth"].to_i
|
19
|
+
@tile_height = tileset_data["tileheight"].to_i
|
20
|
+
|
21
|
+
Logger.trace "Loaded tileset attributes: Name: '#{@name}' First GID: #{@first_gid} Tile Width: #{@tile_width} Tile Height: #{@tile_height}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_image!(image_data)
|
25
|
+
@image_file = File.expand_path("#{MAP_PATH}/#{image_data["source"]}")
|
26
|
+
@image_transparency_color = image_data["trans"]
|
27
|
+
|
28
|
+
@dimensions = ImageUtil.dimensions(@image_file)
|
29
|
+
@tiles_count = @dimensions.first * @dimensions.last / (@tile_width * @tile_height)
|
30
|
+
|
31
|
+
Logger.trace "Loaded tileset image data: File '#{@image_file}' Mask Color: #{@image_transparency_color} Dimensions: #{@dimensions.join(" * ")} Tiles: #{@tiles_count}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Beefdump
|
2
|
+
class ImageUtil
|
3
|
+
|
4
|
+
# credits: http://snippets.dzone.com/posts/show/805
|
5
|
+
def self.dimensions(file)
|
6
|
+
case file
|
7
|
+
when /\.png$/i
|
8
|
+
IO.read(file)[0x10..0x18].unpack('NN')
|
9
|
+
when /\.jpg$/i
|
10
|
+
File.open(file, 'rb') do |io|
|
11
|
+
raise 'malformed JPEG' unless io.getc == 0xFF && io.getc == 0xD8 # SOI
|
12
|
+
|
13
|
+
class << io
|
14
|
+
def readint; (readchar << 8) + readchar; end
|
15
|
+
def readframe; read(readint - 2); end
|
16
|
+
def readsof; [readint, readchar, readint, readint, readchar]; end
|
17
|
+
def next
|
18
|
+
c = readchar while c != 0xFF
|
19
|
+
c = readchar while c == 0xFF
|
20
|
+
c
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
while marker = io.next
|
25
|
+
case marker
|
26
|
+
when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF # SOF markers
|
27
|
+
length, bits, height, width, components = io.readsof
|
28
|
+
raise 'malformed JPEG' unless length == 8 + components * 3
|
29
|
+
when 0xD9, 0xDA: break # EOI, SOS
|
30
|
+
when 0xFE: @comment = io.readframe # COM
|
31
|
+
when 0xE1: io.readframe # APP1, contains EXIF tag
|
32
|
+
else io.readframe # ignore frame
|
33
|
+
end
|
34
|
+
end
|
35
|
+
[width, height]
|
36
|
+
end
|
37
|
+
when /\.bmp$/i
|
38
|
+
d = IO.read(file)[14..28]
|
39
|
+
d[0] == 40 ? d[4..-1].unpack('LL') : d[4..8].unpack('SS')
|
40
|
+
when /\.gif$/i
|
41
|
+
IO.read(file)[6..10].unpack('SS')
|
42
|
+
else
|
43
|
+
raise "Unsupported image type or 'wrong' file name suffix :/"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module World
|
3
|
+
class Object < Entity
|
4
|
+
require 'beefdump/world/object_property'
|
5
|
+
|
6
|
+
attr_reader :type, :properties
|
7
|
+
|
8
|
+
def initialize(object_data)
|
9
|
+
load_object_attributes!(object_data)
|
10
|
+
|
11
|
+
load_properties!(object_data["property"])
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
def load_object_attributes!(object_data)
|
16
|
+
raise "Objects must have a type!" unless @type = object_data["type"].to_sym
|
17
|
+
end
|
18
|
+
|
19
|
+
def load_properties!(properties)
|
20
|
+
return unless properties
|
21
|
+
|
22
|
+
@properties = {}
|
23
|
+
properties.each do |property|
|
24
|
+
load_property!(property)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_property!(property)
|
29
|
+
mandatory = property["mandatory"]
|
30
|
+
mandatory = mandatory.downcase == "true" if mandatory
|
31
|
+
|
32
|
+
name = property["name"].to_sym
|
33
|
+
@properties[name] = ObjectProperty.new(name, property["type"], mandatory)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module World
|
3
|
+
class ObjectProperty
|
4
|
+
TYPES = [:number, :string, :name_reference]
|
5
|
+
|
6
|
+
attr_reader :name, :type
|
7
|
+
|
8
|
+
def initialize(name, type, mandatory = false)
|
9
|
+
raise "Any object property need a name!" unless @name = name
|
10
|
+
|
11
|
+
raise "Object property '#{@name}' has not type. Any property needs a type!" unless type
|
12
|
+
type = type.to_sym
|
13
|
+
raise "Object property type '#{type}' is not supported! Use one of: #{TYPES.join(',')}." unless TYPES.include?(type)
|
14
|
+
@type = type
|
15
|
+
|
16
|
+
@mandatory = !!mandatory
|
17
|
+
end
|
18
|
+
|
19
|
+
def mandatory?
|
20
|
+
@mandatory
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Beefdump
|
2
|
+
module World
|
3
|
+
require 'xmlsimple'
|
4
|
+
require 'beefdump/world/entity'
|
5
|
+
require 'beefdump/world/object'
|
6
|
+
require 'beefdump/world/player'
|
7
|
+
|
8
|
+
# Loads the object descriptions from the objects.xml file in the config folder.
|
9
|
+
def self.load
|
10
|
+
objects_file = "#{CONFIG_PATH}/objects.xml"
|
11
|
+
raise "Objects configuration missing! Please supply '#{objects_file}'." unless File.exist?(objects_file)
|
12
|
+
|
13
|
+
raise "Object configuration format invalid!" unless objectset = XmlSimple.xml_in(objects_file)["object"]
|
14
|
+
@objects = {}
|
15
|
+
objectset.each do |object_data|
|
16
|
+
object = Object.new(object_data)
|
17
|
+
@objects[object.type] = object
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.objects
|
22
|
+
@objects
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: beefdump
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- "Thorben Schr\xC3\xB6der"
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-07-07 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: xml-simple
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
description: A testbed for non-player character AI in role-playing games.
|
36
|
+
email: stillepost@gmail.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- README.md
|
43
|
+
files:
|
44
|
+
- lib/beefdump.rb
|
45
|
+
- lib/beefdump/boot.rb
|
46
|
+
- lib/beefdump/client/base.rb
|
47
|
+
- lib/beefdump/client/graphical/client.rb
|
48
|
+
- lib/beefdump/client/graphical/window.rb
|
49
|
+
- lib/beefdump/config/base.rb
|
50
|
+
- lib/beefdump/config/blueprint.rb
|
51
|
+
- lib/beefdump/game/base.rb
|
52
|
+
- lib/beefdump/game/player.rb
|
53
|
+
- lib/beefdump/game/state.rb
|
54
|
+
- lib/beefdump/logger.rb
|
55
|
+
- lib/beefdump/map/layer.rb
|
56
|
+
- lib/beefdump/map/map.rb
|
57
|
+
- lib/beefdump/map/object.rb
|
58
|
+
- lib/beefdump/map/tileset.rb
|
59
|
+
- lib/beefdump/monkey_patches/open_struct.rb
|
60
|
+
- lib/beefdump/monkey_patches/string.rb
|
61
|
+
- lib/beefdump/utils/image_util.rb
|
62
|
+
- lib/beefdump/world/entity.rb
|
63
|
+
- lib/beefdump/world/object.rb
|
64
|
+
- lib/beefdump/world/object_property.rb
|
65
|
+
- lib/beefdump/world/player.rb
|
66
|
+
- lib/beefdump/world/world.rb
|
67
|
+
- README.md
|
68
|
+
has_rdoc: true
|
69
|
+
homepage: http://github.com/walski/beefdump
|
70
|
+
licenses: []
|
71
|
+
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options:
|
74
|
+
- --charset=UTF-8
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 3
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
requirements: []
|
96
|
+
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 1.3.7
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: A testbed for non-player character AI in role-playing games.
|
102
|
+
test_files: []
|
103
|
+
|