Merge branch 'map_editor2'

Conflicts:
	CorsixTH/Lua/dialogs/resizables/main_menu.lua
	CorsixTH/Lua/map.lua
	CorsixTH/Src/th_lua_map.cpp
	CorsixTH/Src/th_map.h
This commit is contained in:
Stephen E. Baker
2016-02-03 21:43:30 -05:00
19 changed files with 2061 additions and 743 deletions

View File

@@ -110,8 +110,6 @@ else
_DECODA = false
end
_MAP_EDITOR = _MAP_EDITOR or false
-- Enable strict mode
dofile "strict"
require = destrict(require)

View File

@@ -114,6 +114,7 @@ function App:init()
local good_install_folder, error_message = self:checkInstallFolder()
self.good_install_folder = good_install_folder
-- self:checkLanguageFile()
self.level_dir = debug.getinfo(1, "S").source:sub(2, -12) .. "Levels" .. pathsep
self:initSavegameDir()
@@ -141,12 +142,7 @@ function App:init()
modes[#modes + 1] = "opengl"
end
self.fullscreen = false
if _MAP_EDITOR then
MapEditorInitWithLuaApp(self)
modes[#modes + 1] = "reuse context"
self.config.width = 640
self.config.height = 480
elseif self.config.fullscreen then
if self.config.fullscreen then
self.fullscreen = true
modes[#modes + 1] = "fullscreen"
end
@@ -292,41 +288,37 @@ function App:init()
end
-- Load main menu (which creates UI)
if _MAP_EDITOR then
self:loadLevel("")
else
local function callback_after_movie()
self:loadMainMenu()
-- If we couldn't properly load the language, show an information dialog
if not language_load_success then
-- At this point we know the language is english, so no use having
-- localized strings.
self.ui:addWindow(UIInformation(self.ui, {"The game language has been reverted"..
" to English because the desired language could not be loaded. "..
"Please make sure you have specified a font file in the config file."}))
end
local function callback_after_movie()
self:loadMainMenu()
-- If we couldn't properly load the language, show an information dialog
if not language_load_success then
-- At this point we know the language is english, so no use having
-- localized strings.
self.ui:addWindow(UIInformation(self.ui, {"The game language has been reverted"..
" to English because the desired language could not be loaded. "..
"Please make sure you have specified a font file in the config file."}))
end
-- If a savegame was specified, load it
if self.command_line.load then
local status, err = pcall(self.load, self, self.savegame_dir .. self.command_line.load)
if not status then
err = _S.errors.load_prefix .. err
print(err)
self.ui:addWindow(UIInformation(self.ui, {err}))
end
end
-- There might also be a message from the earlier initialization process that should be shown.
-- Show it using the built-in font in case the game's font is messed up.
if error_message then
self.ui:addWindow(UIInformation(self.ui, error_message, true))
-- If a savegame was specified, load it
if self.command_line.load then
local status, err = pcall(self.load, self, self.savegame_dir .. self.command_line.load)
if not status then
err = _S.errors.load_prefix .. err
print(err)
self.ui:addWindow(UIInformation(self.ui, {err}))
end
end
if self.config.play_intro then
self.moviePlayer:playIntro(callback_after_movie)
else
callback_after_movie()
-- There might also be a message from the earlier initialization process that should be shown.
-- Show it using the built-in font in case the game's font is messed up.
if error_message then
self.ui:addWindow(UIInformation(self.ui, error_message, true))
end
end
if self.config.play_intro then
self.moviePlayer:playIntro(callback_after_movie)
else
callback_after_movie()
end
return true
end
@@ -546,14 +538,14 @@ end
-- Loads the specified level. If a string is passed it looks for the file with the same name
-- in the "Levels" folder of CorsixTH, if it is a number it tries to load that level from
-- the original game.
function App:loadLevel(level, ...)
function App:loadLevel(level, difficulty, level_name, level_file, level_intro, map_editor)
if self.world then
self:worldExited()
end
-- Check that we can load the data before unloading current map
local new_map = Map(self)
local map_objects, errors = new_map:load(level, ...)
local map_objects, errors = new_map:load(level, difficulty, level_name, level_file, level_intro, map_editor)
if not map_objects then
self.world.ui:addWindow(UIInformation(self.ui, {errors}))
return
@@ -596,7 +588,7 @@ function App:loadLevel(level, ...)
self.audio:playSoundEffects(self.config.play_sounds)
-- Load UI
self.ui = GameUI(self, self.world:getLocalPlayerHospital())
self.ui = GameUI(self, self.world:getLocalPlayerHospital(), map_editor)
self.world:setUI(self.ui) -- Function call allows world to set up its keyHandlers
if tonumber(level) then
@@ -1368,6 +1360,11 @@ function App:restart()
end))
end
--! Begin the map editor
function App:mapEdit()
self:loadLevel("", nil, nil, nil, nil, true);
end
--! Exits the game completely (no confirmation window)
function App:exit()
-- Save config before exiting

View File

@@ -66,9 +66,7 @@ function Audio:init()
return
end
if not SDL.audio.loaded then
if not _MAP_EDITOR then
print "Notice: Audio system not loaded as CorsixTH compiled without it"
end
print "Notice: Audio system not loaded as CorsixTH compiled without it"
self.not_loaded = true
return
end

View File

@@ -1,663 +0,0 @@
--[[ Copyright (c) 2010 Peter "Corsix" Cawley
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]
dofile "command_stack"
dofile "command"
dofile "commands/set_map_cell"
dofile "commands/set_map_cell_flags"
dofile "commands/compound"
class "UIMapEditor" (Window)
---@type UIMapEditor
local UIMapEditor = _G["UIMapEditor"]
local math_floor
= math.floor
function UIMapEditor:UIMapEditor(ui)
-- Put ourselves in an easily findable global for the UI code to find.
_MAP_EDITOR = self
self:Window()
self.x = 0
self.y = 0
self.width = math.huge
self.height = math.huge
self.ui = ui
self.command_stack = CommandStack()
-- For when there are multiple things which could be sampled from a tile,
-- keep track of the index of which one was most recently sampled, so that
-- next time a different one is sampled.
self.sample_i = 1
-- The block to put on the UI layer as a preview for what will be placed
-- by the current drawing operation.
self.block_brush_preview = 0
self:classifyBlocks()
-- A sprite table containing a "cell outline" sprite
self.cell_outline = TheApp.gfx:loadSpriteTable("Bitmap", "aux_ui", true)
-- Coordinates in Lua tile space of the mouse cursor.
self.mouse_cell_x = 0
self.mouse_cell_y = 0
end
function UIMapEditor:classifyBlocks()
-- Classify each block / tile with a type, subtype, and category.
-- Type and subtype are used by this file, and by the UI code to determine
-- which gallery to put the block in. Category is used purely by the UI to
-- allow subsets of a gallery to be hidden.
local block_info = {}
for i = 1, 15 do
block_info[i] = {"floor", "simple", "Outside"}
end
for i = 16, 23 do
block_info[i] = {"floor", "simple", "Inside"}
end
for i = 24, 40 do
block_info[i] = {"object"}
end
for i = 41, 58 do
block_info[i] = {"floor", "simple", "Road"}
end
block_info[59] = {"floor", "decorated", "Pond"}
block_info[60] = {"floor", "decorated", "Pond"}
for i = 61, 64 do
block_info[i] = {"floor", "simple", "Outside"}
end
block_info[65] = {"floor", "decorated", "Pond"}
block_info[66] = {"floor", "simple", "Inside"} -- 67 is UI.
block_info[68] = {"floor", "decorated", "Pond"}
block_info[69] = {"floor", "decorated", "Pond"}
block_info[70] = {"floor", "simple", "Inside"}
for i = 71, 73 do
block_info[i] = {"floor", "decorated", "Pond", base = 69}
end
block_info[76] = {"floor", "simple", "Inside"}
for i = 77, 80 do
block_info[i] = {"floor", "decorated", "Pond"}
end
for i = 114, 164 do -- 82-113 are internal walls
local pair
local category = "External"
local dir = i % 2 == 0 and "north" or "west"
if 114 <= i and i <= 127 then
if 114 <= i and i <= 119 then
pair = i + 8
elseif 122 <= i and i < 127 then
pair = i - 8
end
elseif 157 <= i and i <= 164 then
category = "Doorway"
if 157 <= i and i <= 160 then
pair = i + 4
elseif 161 <= i and i < 164 then
pair = i - 4
end
dir = dir == "north" and "west" or "north"
end
for i = 128, 156 do
block_info[i] = {"object"}
end
for i = 120, 121 do -- removes the complete windows as they disappear in the game
block_info[i] = {"object"}
end
if i ~= 144 and i ~= 145 and i ~= 156 then
block_info[i] = {"wall", dir, category, pair = pair}
end
end
for i = 176, 191 do
block_info[i] = {"floor", "decorated", "Hegderow", base = 2}
end
for i = 192, 196 do
block_info[i] = {"floor", "decorated", "Foliage", base = 2}
end
for i = 197, 204 do
block_info[i] = {"floor", "decorated", "Foliage", base = 2}
end
for i = 205, 208 do
block_info[i] = {"floor", "simple", "Outside"}
end
block_info[208].base = 3
-- adds street lights, could do with mirrors of these to have lamps facing different directions
for i = 209, 210 do
local pair
local category = "External"
local dir = i % 2 == 0 and "north" or "west"
if i ~= 209 then
pair = i - 1
end
block_info[i] = {"wall", dir, category, pair = pair}
end
MapEditorSetBlocks(self.ui.app.map.blocks, block_info) -- pass data to UI
self.block_info = block_info
end
function UIMapEditor:draw(canvas, ...)
local ui = self.ui
local x, y = ui:WorldToScreen(self.mouse_cell_x, self.mouse_cell_y)
self.cell_outline:draw(canvas, 2, x - 32, y)
Window.draw(self, canvas, ...)
end
function UIMapEditor:onMouseMove(x, y, dx, dy)
local repaint = Window.onMouseMove(self, x, y, dx, dy)
local ui = self.ui
local wxr, wyr = ui:ScreenToWorld(self.x + x, self.y + y)
local wx = math_floor(wxr)
local wy = math_floor(wyr)
local map = self.ui.app.map
-- Update the stored state of cursor position, and trigger a repaint as the
-- cell outline sprite should track the cursor position.
if wx ~= self.mouse_cell_x or wy ~= self.mouse_cell_y then
repaint = true
self.mouse_cell_x = wx
self.mouse_cell_y = wy
-- Right button down: sample the block under the cursor (unless left is
-- held, as that indicates a paint in progress).
if self.buttons_down.mouse_right and not self.buttons_down.mouse_left then
self:sampleBlock(x, y)
end
end
-- Left button down: Expand / contract the rectangle which will be painted to
-- include the original mouse down cell and the current cell, or clear the
-- rectangle if the cursor is returned to where the mouse down position.
if self.buttons_down.mouse_left and self.paint_rect and 1 <= wx and 1 <= wy
and wx <= map.width and wy <= map.height then
local p1x = math_floor(self.paint_start_wx)
local p1y = math_floor(self.paint_start_wy)
local p2x = wx
local p2y = wy
local x, y, w, h
if p1x < p2x then
x = p1x
w = p2x - p1x + 1
else
x = p2x
w = p1x - p2x + 1
end
if p1y < p2y then
y = p1y
h = p2y - p1y + 1
else
y = p2y
h = p1y - p2y + 1
end
if w*h > 1 then
-- The paint rectangle extends beyond the original mouse down cell, so
-- make the area in the middle of said cell into a "null area" so that
-- the paint operation can be cancelled by returning the cursor to this
-- area and ending the drag.
self.has_paint_null_area = true
elseif w == 1 and h == 1 and self.has_paint_null_area then
if ((wxr % 1) - 0.5)^2 + ((wyr % 1) - 0.5)^2 < 0.25^2 then
w = 0
h = 0
end
end
local rect = self.paint_rect
if x ~= rect.x or y ~= rect.y or w ~= rect.w or h ~= rect.h then
self:setPaintRect(x, y, w, h)
repaint = true
end
end
return repaint
end
function UIMapEditor:onMouseDown(button, x, y)
local repaint = false
local map = self.ui.app.map.th
if button == "right" then
if self.buttons_down.mouse_left then
-- Right click while left is down: set paint step size
local wx, wy = self.ui:ScreenToWorld(x, y)
local xstep = math.max(1, math.abs(math.floor(wx) - math.floor(self.paint_start_wx)))
local ystep = math.max(1, math.abs(math.floor(wy) - math.floor(self.paint_start_wy)))
local rect = self.paint_rect
self:setPaintRect(rect.x, rect.y, rect.w, rect.h, xstep, ystep)
repaint = true
else
-- Normal right click: sample the block under the cursor
self:sampleBlock(x, y)
end
elseif button == "left" then
self.current_command = CompoundCommand()
self.current_command_cell = SetMapCellCommand(map)
self.current_command_cell_flags = SetMapCellFlagsCommand(map)
self:startPaint(x, y)
repaint = true
elseif button == "left_double" then
self.current_command = CompoundCommand()
self.current_command_cell = SetMapCellCommand(map)
self.current_command_cell_flags = SetMapCellFlagsCommand(map)
self:doLargePaint(x, y)
-- Set a dummy paint rect for the mouse up event.
self:setPaintRect(0, 0, 0, 0)
repaint = true
end
return Window.onMouseDown(self, button, x, y) or repaint
end
function UIMapEditor:onMouseUp(button, x, y)
local repaint = false
if button == "left" and self.paint_rect then
self:finishPaint(true)
if #self.current_command_cell.paint_list ~=0 then
self.current_command:addCommand(self.current_command_cell)
end
if #self.current_command_cell_flags.paint_list ~= 0 then
self.current_command:addCommand(self.current_command_cell_flags)
end
self.command_stack:add(self.current_command)
repaint = true
end
return Window.onMouseUp(self, button, x, y) or repaint
end
function UIMapEditor:startPaint(x, y)
-- Save the Lua world coordinates of where the painting started.
self.paint_start_wx, self.paint_start_wy = self.ui:ScreenToWorld(x, y)
-- Initialise an empty paint rectangle.
self.paint_rect = {
x = math_floor(self.paint_start_wx),
y = math_floor(self.paint_start_wy),
w = 0, h = 0,
}
self.has_paint_null_area = false
-- Check that starting point isn't out of bounds
local map = self.ui.app.map
if self.paint_rect.x < 1 or self.paint_rect.y < 1
or self.paint_rect.x > map.width or self.paint_rect.y > map.height then
self.paint_rect = nil
return
end
-- Reset the paint step.
self.paint_step_x = 1
self.paint_step_y = 1
-- Extend the paint rectangle to the single cell.
self:setPaintRect(self.paint_rect.x, self.paint_rect.y, 1, 1)
end
-- Injective function from NxN to N which allows for a pair of positive
-- integers to be combined into a single value suitable for use as a key in a
-- table.
local function combine_ints(x, y)
local sum = x + y
return y + sum * (sum + 1) / 2
end
function UIMapEditor:doLargePaint(x, y)
-- Perform a "large" paint. At the moment, this is triggered by a double
-- left click, and results in a floor tile "flood fill" operation.
-- Get the Lua tile coordinate of the tile to start filling from.
x, y = self.ui:ScreenToWorld(x, y)
x = math_floor(x)
y = math_floor(y)
if x <= 0 or y <= 0 then
return
end
-- The click which preceeded the double click should have set "old_floors"
-- with the contents of the tile prior to the single click.
if not self.old_floors then
return
end
-- brush_f is the tile which is to be painted
local brush_f = self.block_brush_f
if not brush_f or brush_f == 0 then
return
end
local map = self.ui.app.map.th
local key = combine_ints(x, y)
-- match_f is the tile which is to be replaced by brush_f
local match_f = self.old_floors[key]
if not match_f then
return
end
match_f = match_f % 256 -- discard shadow flags, etc.
-- If the operation wouldn't change anything, don't do it.
if match_f == brush_f then
return
end
-- Reset the starting tile as to simplify the upcoming loop.
self.current_command_cell:addTile(x, y, 1, match_f)
map:setCell(x, y, 1, match_f)
local to_visit = {[key] = {x, y}}
local visited = {[key] = true}
-- Mark the tiles beyond the edge of the map as visited, as to prevent the
-- pathfinding from exceeding the bounds of the map.
local size = map:size()
for i = 1, size do
visited[combine_ints(0, i)] = true
visited[combine_ints(i, 0)] = true
visited[combine_ints(size + 1, i)] = true
visited[combine_ints(i, size + 1)] = true
end
-- When a tile is added to "visited" to the first time, also add it to
-- "to_visit". This ensures each tile is visited no more than once.
setmetatable(visited, {__newindex = function(t, k, v)
rawset(t, k, v)
to_visit[k] = v
end})
-- Iterate over the tiles to visit, and if they are suitable for replacement,
-- do the replacement and add their neighbours to the list of things to do.
repeat
x = to_visit[key][1]
y = to_visit[key][2]
to_visit[key] = nil
local f = map:getCell(x, y)
if f % 256 == match_f then
self.current_command_cell:addTile(x, y, 1, f - match_f + brush_f)
map:setCell(x, y, 1, f - match_f + brush_f)
visited[combine_ints(x, y + 1)] = {x, y + 1}
visited[combine_ints(x, y - 1)] = {x, y - 1}
visited[combine_ints(x + 1, y)] = {x + 1, y}
visited[combine_ints(x - 1, y)] = {x - 1, y}
end
to_visit[key] = nil
key = next(to_visit)
until not key
end
-- Remove the paint preview from the UI layer, and optionally apply the paint
-- to the actual layers.
function UIMapEditor:finishPaint(apply)
-- Grab local copies of all the fields which we need
local map = self.ui.app.map.th
local brush_f = self.block_brush_f
local brush_parcel = self.block_brush_parcel
if brush_parcel then
brush_parcel = {parcelId = brush_parcel}
end
local brush_w1 = self.block_brush_w1
local brush_w1p = (self.block_info[brush_w1] or '').pair
local brush_w2 = self.block_brush_w2
local brush_w2p = (self.block_info[brush_w2] or '').pair
local x_first, x_last = self.paint_rect.x, self.paint_rect.x + self.paint_rect.w - 1
local y_first, y_last = self.paint_rect.y, self.paint_rect.y + self.paint_rect.h - 1
local step_base_x = math.floor(self.paint_start_wx)
local step_base_y = math.floor(self.paint_start_wy)
local xstep = self.paint_step_x
local ystep = self.paint_step_y
-- Determine what kind of thing is being painted.
local is_wall = self.block_info[self.block_brush_preview]
is_wall = is_wall and is_wall[1] == "wall" and is_wall[2]
local is_simple_floor = self.block_info[self.block_brush_preview]
is_simple_floor = is_simple_floor and is_simple_floor[1] == "floor" and is_simple_floor[2] == "simple"
-- To allow the double click handler to know what was present before the
-- single click which preceeds it, the prior contents of the floor layer is
-- saved.
local old_floors = {}
self.old_floors = old_floors
for tx = x_first, x_last do
for ty = y_first, y_last do
-- Grab and save the contents of the tile
local f, w1, w2 = map:getCell(tx, ty)
old_floors[combine_ints(tx, ty)] = f
local flags
-- Change the contents according to what is being painted
repeat
-- If not painting, do not change anything (apart from UI layer).
if not apply then
break
end
-- If the paint is happening at an interval, do not change things
-- apart from at the occurances of the interval.
if ((tx - step_base_x) % xstep) ~= 0 then
break
end
if ((ty - step_base_y) % ystep) ~= 0 then
break
end
flags = brush_parcel
-- If painting walls, only apply to the two edges which get painted.
if is_wall == "north" and ty ~= y_first and ty ~= y_last then
break
end
if is_wall == "west" and tx ~= x_first and tx ~= x_last then
break
end
-- If painting a floor component, apply it.
if not brush_parcel and brush_f and brush_f ~= 0 then
f = f - (f % 256) + brush_f
-- If painting just a floor component, then remove any decoration
-- and/or walls which get painted over.
if is_simple_floor then
local w1b = self.block_info[w1 % 256]
if not w1b or w1b[1] ~= "wall" or ty ~= y_first then
w1 = w1 - (w1 % 256)
end
local w2b = self.block_info[w2 % 256]
if not w2b or w2b[1] ~= "wall" or tx ~= x_first then
w2 = w2 - (w2 % 256)
end
end
end
-- If painting wall components, apply them.
if brush_w1 and brush_w1 ~= 0 then
w1 = w1 - (w1 % 256) + (ty ~= step_base_y and brush_w1p or brush_w1)
end
if brush_w2 and brush_w2 ~= 0 then
w2 = w2 - (w2 % 256) + (tx ~= step_base_x and brush_w2p or brush_w2)
end
until true
-- Remove the UI layer and perform the adjustment of the other layers.
self.current_command_cell:addTile(tx,ty,f,w1,w2,0)
map:setCell(tx, ty, f, w1, w2, 0)
if flags then
self.current_command_cell_flags:addTile(tx, ty, flags)
map:setCellFlags(tx, ty, flags)
end
end
end
map:updateShadows()
end
-- Move/resize the rectangle to be painted, and update the UI preview layer
-- to reflect the new rectangle.
function UIMapEditor:setPaintRect(x, y, w, h, xstep, ystep)
local map = self.ui.app.map.th
local rect = self.paint_rect
local old_xstep = self.paint_step_x or 1
local old_ystep = self.paint_step_y or 1
local step_base_x = math.floor(self.paint_start_wx)
local step_base_y = math.floor(self.paint_start_wy)
xstep = xstep or old_xstep
ystep = ystep or old_ystep
-- Create a rectangle which contains both the old and new rectangles, as
-- this contains all tiles which may need to change.
local left, right, top, bottom = x, x + w - 1, y, y + h - 1
if rect then
if rect.x < left then
left = rect.x
end
if rect.x + rect.w - 1 > right then
right = rect.x + rect.w - 1
end
if rect.y < top then
top = rect.y
end
if rect.y + rect.h - 1 > bottom then
bottom = rect.y + rect.h - 1
end
end
-- Determine what kind of thing is being painted
local is_wall = self.block_info[self.block_brush_preview]
local block_brush_preview_pair = is_wall and is_wall.pair
is_wall = is_wall and is_wall[1] == "wall" and is_wall[2]
for tx = left, right do
for ty = top, bottom do
local now_in, was_in
-- Non-walls: paint at every tile within the rectangle
if not is_wall then
now_in = (x <= tx and tx < x + w and y <= ty and ty < y + h)
was_in = (rect.x <= tx and tx < rect.x + rect.w and rect.y <= ty and ty < rect.y + rect.h)
-- Walls: paint at two edges of the rectangle
elseif is_wall == "north" then
now_in = (x <= tx and tx < x + w and (y == ty or ty == y + h - 1))
was_in = (rect.x <= tx and tx < rect.x + rect.w and (rect.y == ty or ty == rect.y + rect.h - 1))
elseif is_wall == "west" then
now_in = ((x == tx or tx == x + w - 1) and y <= ty and ty < y + h)
was_in = ((rect.x == tx or tx == rect.x + rect.w - 1) and rect.y <= ty and ty < rect.y + rect.h)
end
-- Restrict the paint to tiles which fall on the appropriate intervals
now_in = now_in and ((tx - step_base_x) % xstep) == 0
now_in = now_in and ((ty - step_base_y) % ystep) == 0
was_in = was_in and ((tx - step_base_x) % old_xstep) == 0
was_in = was_in and ((ty - step_base_y) % old_ystep) == 0
-- Update the tile, but only if it needs changing
if now_in ~= was_in then
local ui_layer = 0
if now_in then
local brush = self.block_brush_preview
if is_wall == "north" and ty ~= step_base_y then
brush = block_brush_preview_pair or brush
end
if is_wall == "west" and tx ~= step_base_x then
brush = block_brush_preview_pair or brush
end
ui_layer = brush + 256 * DrawFlags.Alpha50
end
self.current_command_cell:addTile(tx, ty, 4, ui_layer)
map:setCell(tx, ty, 4, ui_layer)
end
end
end
-- Save the details of the new rectangle
if not rect then
rect = {}
self.paint_rect = rect
end
rect.x = x
rect.y = y
rect.w = w
rect.h = h
self.paint_step_x = xstep
self.paint_step_y = ystep
end
function UIMapEditor:sampleBlock(x, y)
local ui = self.ui
local wx, wy = self.ui:ScreenToWorld(x, y)
wx = math_floor(wx)
wy = math_floor(wy)
local map = self.ui.app.map
if wx < 1 or wy < 1 or wx > map.width or wy > map.height then
return
end
local floor, wall1, wall2 = map.th:getCell(wx, wy)
local set = {}
set[floor % 256] = true
set[wall1 % 256] = true
set[wall2 % 256] = true
if wx < map.width then
floor, wall1, wall2 = map.th:getCell(wx + 1, wy)
set[wall2 % 256] = true
end
if wy < map.height then
floor, wall1, wall2 = map.th:getCell(wx, wy + 1)
set[wall1 % 256] = true
end
set[0] = nil
local floor_list = {}
local wall_list = {}
for i in pairs(set) do
if self.block_info[i] then
if self.block_info[i][1] == "floor" then
floor_list[#floor_list + 1] = i
elseif self.block_info[i][1] == "wall" then
wall_list[#wall_list + 1] = i
end
end
end
if wx == self.recent_sample_x and wy == self.recent_sample_y then
self.sample_i = self.sample_i + 1
else
self.sample_i = 1
self.recent_sample_x = wx
self.recent_sample_y = wy
end
MapEditorSetBlockBrush(
floor_list[1 + (self.sample_i - 1) % #floor_list] or 0,
wall_list [1 + (self.sample_i - 1) % #wall_list ] or 0
)
end
-- Called by the UI to set what should be painted.
function UIMapEditor:setBlockBrush(f, w1, w2)
local preview = f
if w2 ~= 0 then
preview = w2
elseif w1 ~= 0 then
preview = w1
end
self.block_brush_preview = preview
self.block_brush_parcel = nil
self.block_brush_f = f
self.block_brush_w1 = w1
self.block_brush_w2 = w2
end
function UIMapEditor:setBlockBrushParcel(parcel)
self.block_brush_preview = 24
self.block_brush_parcel = parcel
self.block_brush_f = self.block_brush_preview
self.block_brush_w1 = 0
self.block_brush_w2 = 0
end
function UIMapEditor:undo()
local last = self.command_stack:undo()
self.ui.app.map.th:updateShadows()
return last
end
function UIMapEditor:redo()
local last = self.command_stack:redo()
self.ui.app.map.th:updateShadows()
return last
end

View File

@@ -28,7 +28,7 @@ class "UIMenuBar" (Window)
---@type UIMenuBar
local UIMenuBar = _G["UIMenuBar"]
function UIMenuBar:UIMenuBar(ui)
function UIMenuBar:UIMenuBar(ui, map_editor)
self:Window()
local app = ui.app
@@ -51,7 +51,11 @@ function UIMenuBar:UIMenuBar(ui)
-- This list satifies: open_menus[x] == nil or open_menus[x].level == x
self.open_menus = {}
self:makeMenu(app)
if map_editor then
self:makeMapeditorMenu(app)
else
self:makeGameMenu(app)
end
end
function UIMenuBar:onTick()
@@ -521,7 +525,26 @@ function UIMenu:appendMenu(text, menu)
}
end
function UIMenuBar:makeMenu(app)
--! Make a menu for the map editor.
--!param app Application.
function UIMenuBar:makeMapeditorMenu(app)
local menu = UIMenu()
menu:appendItem(_S.menu_file.load, function() self.ui:addWindow(UILoadMap(self.ui, "map")) end)
:appendItem(_S.menu_file.save, function() self.ui:addWindow(UISaveMap(self.ui)) end)
:appendItem(_S.menu_file.quit, function() self.ui:quit() end)
self:addMenu(_S.menu.file, menu)
menu = UIMenu()
menu:appendItem(_S.menu_player_count.players_1, function() self.ui.map_editor:setPlayerCount(1) end)
:appendItem(_S.menu_player_count.players_2, function() self.ui.map_editor:setPlayerCount(2) end)
:appendItem(_S.menu_player_count.players_3, function() self.ui.map_editor:setPlayerCount(3) end)
:appendItem(_S.menu_player_count.players_4, function() self.ui.map_editor:setPlayerCount(4) end)
self:addMenu(_S.menu.player_count, menu)
end
--! Make a menu for the game.
--!param app Application.
function UIMenuBar:makeGameMenu(app)
local menu = UIMenu()
menu:appendItem(_S.menu_file.load, function() self.ui:addWindow(UILoadGame(self.ui, "game")) end)
:appendItem(_S.menu_file.save, function() self.ui:addWindow(UISaveGame(self.ui)) end)
@@ -754,7 +777,7 @@ function UIMenuBar:makeMenu(app)
:appendCheckItem(_S.menu_debug_overlay.byte_5, false, overlay(35, 8, 5, 5, true), "")
:appendCheckItem(_S.menu_debug_overlay.byte_6, false, overlay(35, 8, 6, 6, true), "")
:appendCheckItem(_S.menu_debug_overlay.byte_7, false, overlay(35, 8, 7, 7, true), "")
:appendCheckItem(_S.menu_debug_overlay.parcel, false, overlay(131107, 2, 0, 0, false), "")
:appendCheckItem(_S.menu_debug_overlay.parcel, false, overlay("parcel"), "")
)
:appendItem(_S.menu_debug.sprite_viewer, function() dofile "sprite_viewer" end)
)

View File

@@ -0,0 +1,56 @@
--[[ Copyright (c) 2010 Manuel "Roujin" Wolf
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]
--! Load Map Window
class "UILoadMap" (UIFileBrowser)
---@type UILoadMap
local UILoadMap = _G["UILoadMap"]
function UILoadMap:UILoadMap(ui, mode)
local path = ui.app.level_dir
local treenode = FilteredFileTreeNode(path, ".map")
treenode.label = "Maps"
self:UIFileBrowser(ui, mode, _S.load_game_window.caption:format(".map"), 295, treenode, true)
-- The most probable preference of sorting is by date - what you played last
-- is the thing you want to play soon again.
self.control:sortByDate()
end
function UILoadMap:choiceMade(name)
local app = self.ui.app
-- Make sure there is no blue filter active.
app.video:setBlueFilterActive(false)
name = name:sub(app.level_dir:len() + 1)
local status, err = pcall(app.loadLevel, app, name, nil, nil, nil, nil, true)
if not status then
err = _S.errors.load_prefix .. err
print(err)
app:loadMainMenu()
app.ui:addWindow(UIInformation(self.ui, {err}))
end
end
function UILoadMap:close()
UIResizable.close(self)
if self.mode == "menu" then
self.ui:addWindow(UIMainMenu(self.ui))
end
end

View File

@@ -0,0 +1,106 @@
--[[ Copyright (c) 2015 Stephen E. Baker et al.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]
--! Save Map Window
class "UISaveMap" (UIFileBrowser)
---@type UISaveMap
local UISaveMap = _G["UISaveMap"]
local pathsep = package.config:sub(1, 1)
local col_textbox = {
red = 0,
green = 0,
blue = 0,
}
local col_highlight = {
red = 174,
green = 166,
blue = 218,
}
local col_shadow = {
red = 134,
green = 126,
blue = 178,
}
function UISaveMap:UISaveMap(ui)
local treenode = FilteredFileTreeNode(ui.app.level_dir, ".map")
treenode.label = "Maps"
self:UIFileBrowser(ui, "map", _S.save_map_window.caption:format(".map"), 265, treenode, true)
-- The most probable preference of sorting is by date - what you played last
-- is the thing you want to play soon again.
self.control:sortByDate()
-- Textbox for entering new save name
self.new_map_textbox = self:addBevelPanel(5, 310, self.width - 10, 17, col_textbox, col_highlight, col_shadow)
:setLabel(_S.save_map_window.new_map, nil, "left"):setTooltip(_S.tooltip.save_map_window.new_map)
:makeTextbox(--[[persistable:save_map_new_map_textbox_confirm_callback]] function() self:confirmName() end,
--[[persistable:save_map_new_map_textbox_abort_callback]] function() self:abortName() end)
end
--! Function called when textbox is aborted (e.g. by pressing escape)
function UISaveMap:abortName()
self.new_map_textbox.text = ""
self.new_map_textbox.panel:setLabel(_S.save_map_window.new_map)
end
--! Function called when textbox is confirmed (e.g. by pressing enter)
function UISaveMap:confirmName()
local filename = self.new_map_textbox.text
local app = self.ui.app
if filename == "" then
self:abortName()
return
end
self:trySave(app.level_dir .. filename .. ".map")
end
--! Function called by clicking button of existing save #num
function UISaveMap:choiceMade(name)
self:trySave(name)
end
--! Try to save the game with given filename; if already exists, create confirmation window first.
function UISaveMap:trySave(filename)
if lfs.attributes(filename, "size") ~= nil then
self.ui:addWindow(UIConfirmDialog(self.ui, _S.confirmation.overwrite_save, --[[persistable:save_map_confirmation]] function() self:doSave(filename) end))
else
self:doSave(filename)
end
end
--! Actually do save the map with given filename.
function UISaveMap:doSave(filename)
filename = filename
local ui = self.ui
local map = ui.app.map
self:close()
local status, err = pcall(map.save, map, filename)
if not status then
err = _S.errors.save_prefix .. err
print(err)
ui:addWindow(UIInformation(ui, {err}))
end
end

View File

@@ -43,6 +43,7 @@ function UIMainMenu:UIMainMenu(ui)
{_S.main_menu.continue, self.buttonContinueGame, _S.tooltip.main_menu.continue},
{_S.main_menu.load_game, self.buttonLoadGame, _S.tooltip.main_menu.load_game},
{_S.main_menu.options, self.buttonOptions, _S.tooltip.main_menu.options},
{_S.main_menu.map_edit, self.buttonMapEdit, _S.tooltip.main_menu.map_edit},
{_S.main_menu.exit, self.buttonExit, _S.tooltip.main_menu.exit}
}
self.no_menu_entries = #menu_items
@@ -149,6 +150,10 @@ function UIMainMenu:buttonOptions()
self.ui:addWindow(window)
end
function UIMainMenu:buttonMapEdit()
self.ui.app:mapEdit()
end
function UIMainMenu:buttonExit()
self.ui:addWindow(UIConfirmDialog(self.ui,
_S.tooltip.main_menu.quit,

File diff suppressed because it is too large Load Diff

View File

@@ -39,9 +39,7 @@ function UICustomGame:UICustomGame(ui)
self.label_font = TheApp.gfx:loadFont("QData", "Font01V")
-- Supply the required list of items to UIMenuList
local path = debug.getinfo(1, "S").source:sub(2, -57)
path = path .. "Levels" .. pathsep
local path = ui.app.level_dir
-- Create the actual list
local items = {}

View File

@@ -32,30 +32,36 @@ local WM = SDL.wm
local lfs = require "lfs"
local pathsep = package.config:sub(1, 1)
function GameUI:GameUI(app, local_hospital)
--! Game UI constructor.
--!param app (Application) Application object.
--!param local_hospital Hospital to display
--!param map_editor (bool) Whether the map is editable.
function GameUI:GameUI(app, local_hospital, map_editor)
self:UI(app)
self.hospital = local_hospital
self.tutorial = { chapter = 0, phase = 0 }
if _MAP_EDITOR then
self:addWindow(UIMapEditor(self))
if map_editor then
self.map_editor = UIMapEditor(self)
self:addWindow(self.map_editor)
else
self.adviser = UIAdviser(self)
self.bottom_panel = UIBottomPanel(self)
self.menu_bar = UIMenuBar(self)
self.bottom_panel:addWindow(self.adviser)
self:addWindow(self.bottom_panel)
self:addWindow(self.menu_bar)
end
-- UI widgets
self.menu_bar = UIMenuBar(self, self.map_editor)
self:addWindow(self.menu_bar)
local scr_w = app.config.width
local scr_h = app.config.height
self.visible_diamond = self:makeVisibleDiamond(scr_w, scr_h)
if self.visible_diamond.w <= 0 or self.visible_diamond.h <= 0 then
-- For a standard 128x128 map, screen size would have to be in the
-- region of 3276x2457 in order to be too large.
if not _MAP_EDITOR then
if not self.map_editor then
error "Screen size too large for the map"
end
end
@@ -63,7 +69,7 @@ function GameUI:GameUI(app, local_hospital)
app.map.th:getCameraTile(local_hospital:getPlayerIndex()))
self.zoom_factor = 1
self:scrollMap(-scr_w / 2, 16 - scr_h / 2)
self.limit_to_visible_diamond = not _MAP_EDITOR
self.limit_to_visible_diamond = not self.map_editor
self.transparent_walls = false
self.do_world_hit_test = true
@@ -141,7 +147,7 @@ end
function GameUI:draw(canvas)
local app = self.app
local config = app.config
if _MAP_EDITOR or not self.in_visible_diamond then
if self.map_editor or not self.in_visible_diamond then
canvas:fillBlack()
end
local zoom = self.zoom_factor
@@ -505,7 +511,7 @@ function GameUI:onMouseUp(code, x, y)
end
local button = self.button_codes[code]
if button == "right" and not _MAP_EDITOR and highlight_x then
if button == "right" and not self.map_editor and highlight_x then
local window = self:getWindow(UIPatient)
local patient = (window and window.patient.is_debug and window.patient) or self.hospital:getDebugPatient()
if patient then

View File

@@ -845,9 +845,9 @@ end
function Hospital:getHeliportPosition()
local x, y = self.world.map.th:getHeliportTile(self:getPlayerIndex())
-- NB: Level 2 has a heliport tile set, but no heliport, so we ensure that
-- the specified tile is suitable by checking the adjacent spawn tile for
-- the specified tile is suitable by checking the spawn tile for
-- passability.
if y > 1 and self.world.map:getCellFlag(x, y - 1, "passable") then
if y > 0 and self.world.map:getCellFlag(x, y, "passable") then
return x, y
end
end

View File

@@ -80,6 +80,8 @@ tooltip.casebook.cure_type.unknown = "You do not yet know how to treat this dise
tooltip.research_policy.no_research = "No research is being carried out in this category at the moment"
tooltip.research_policy.research_progress = "Progress towards the next discovery in this category: %1%/%2%"
menu["player_count"] = "PLAYER COUNT"
menu_file = {
load = " (SHIFT+L) LOAD ",
save = " (SHIFT+S) SAVE ",
@@ -164,6 +166,12 @@ menu_debug_overlay = {
byte_7 = " BYTE 7 ",
parcel = " PARCEL ",
}
menu_player_count = {
players_1 = " 1 PLAYER ",
players_2 = " 2 PLAYERS ",
players_3 = " 3 PLAYERS ",
players_4 = " 4 PLAYERS ",
}
adviser = {
room_forbidden_non_reachable_parts = "Placing the room in this location would result in parts of the hospital not being reachable.",
warnings = {
@@ -253,6 +261,7 @@ main_menu = {
continue = "Continue Game",
load_game = "Load Game",
options = "Settings",
map_edit = "Map Editor",
savegame_version = "Savegame version: ",
version = "Version: ",
exit = "Exit",
@@ -265,6 +274,7 @@ tooltip.main_menu = {
continue = "Continue your latest saved game",
load_game = "Load a saved game",
options = "Tweak various settings",
map_edit = "Create a custom map",
exit = "No, no, please don't leave!",
quit = "You are about to quit from CorsixTH. Are you sure this is what you want to do?",
}
@@ -311,6 +321,16 @@ tooltip.save_game_window = {
new_save_game = "Enter name for a new savegame",
}
save_map_window = {
caption = "Save Map (%1%)",
new_map = "New Map",
}
tooltip.save_map_window = {
map = "Overwrite map %s",
new_map = "Enter name for a map savegame",
}
menu_list_window = {
name = "Name",
save_date = "Modified",
@@ -631,6 +651,40 @@ tooltip.update_window = {
ignore = "Ignore this update for now. You will be notified again when you next open CorsixTH",
}
map_editor_window = {
pages = {
inside = "Inside",
outside = "Outside",
foliage = "Foliage",
hedgerow = "Hedgerow",
pond = "Pond",
road = "Road",
north_wall = "North wall",
west_wall = "West wall",
helipad = "Helipad",
delete_wall = "Delete walls",
parcel_0 = "Parcel 0",
parcel_1 = "Parcel 1",
parcel_2 = "Parcel 2",
parcel_3 = "Parcel 3",
parcel_4 = "Parcel 4",
parcel_5 = "Parcel 5",
parcel_6 = "Parcel 6",
parcel_7 = "Parcel 7",
parcel_8 = "Parcel 8",
parcel_9 = "Parcel 9",
camera_1 = "Camera 1",
camera_2 = "Camera 2",
camera_3 = "Camera 3",
camera_4 = "Camera 4",
heliport_1 = "Heliport 1",
heliport_2 = "Heliport 2",
heliport_3 = "Heliport 3",
heliport_4 = "Heliport 4",
paste = "Paste area",
}
}
-------------------------------- UNUSED -----------------------------------
------------------- (kept for backwards compatibility) ----------------------

View File

@@ -60,6 +60,30 @@ function Map:getRoomId(x, y)
return self.th:getCellFlags(math.floor(x), math.floor(y)).roomId
end
function Map:setPlayerCount(count)
self.th:setPlayerCount(count)
end
function Map:getPlayerCount(count)
self.th:getPlayerCount(count)
end
--! Set the camera tile for the given player on the map
--!param x (int) Horizontal position of tile to set camera on
--!param y (int) Vertical position of the tile to set the camera on
--!param player (int) Player number (1-4)
function Map:setCameraTile(x, y, player)
self.th:setCameraTile(x, y, player);
end
--! Set the heliport tile for the given player on the map
--!param x (int) Horizontal position of tile to set heliport on
--!param y (int) Vertical position of the tile to set the heliport on
--!param player (int) Player number (1-4)
function Map:setHeliportTile(x, y, player)
self.th:setHeliportTile(x, y, player);
end
--! Set how to display the room temperature in the hospital map.
--!param method (int) Way of displaying the temperature. See also THMapTemperatureDisplay enum.
--! 1=red gradients, 2=blue/green/red colour shifts, 3=yellow/orange/red colour shifts
@@ -141,7 +165,7 @@ the original game levels are considered.
!param level_intro (string) If loading a custom level this message will be shown as soon as the level
has been loaded.
]]
function Map:load(level, difficulty, level_name, map_file, level_intro)
function Map:load(level, difficulty, level_name, map_file, level_intro, map_editor)
local objects, i
if not difficulty then
difficulty = "full"
@@ -197,14 +221,19 @@ function Map:load(level, difficulty, level_name, map_file, level_intro)
errors, result = self:loadMapConfig(p, result, true)
self.level_config = result
end
elseif _MAP_EDITOR then
elseif map_editor then
-- We're being fed data by the map editor.
self.level_name = "MAP EDITOR"
self.level_number = "MAP EDITOR"
if level == "" then
i, objects = self.th:loadBlank()
else
i, objects = self.th:load(level)
local data, errors = self:getRawData(level)
if data then
i, objects = self.th:load(data)
else
return nil, errors
end
end
assert(base_config, "No base config has been loaded!")
@@ -234,7 +263,7 @@ function Map:load(level, difficulty, level_name, map_file, level_intro)
self.parcelTileCounts = {}
for plot = 1, self.th:getPlotCount() do
self.parcelTileCounts[plot] = self.th:getParcelTileCount(plot)
if plot > 1 and not _MAP_EDITOR then
if plot > 1 and not map_editor then
-- TODO: On multiplayer maps, assign plots 2-N to players 2-N
self.th:setPlotOwner(plot, 0)
end
@@ -243,6 +272,13 @@ function Map:load(level, difficulty, level_name, map_file, level_intro)
return objects
end
--[[! Saves the map to a .map file
!param filename (string) Name of the file to save the map in
]]
function Map:save(filename)
self.th:save(filename)
end
--[[! Loads map configurations from files. Returns nil as first result
if no configuration could be loaded and config as second result no matter what.
!param filename (string) The absolute path to the config file to load
@@ -370,6 +406,50 @@ function Map:updateDebugOverlayHeat()
end
end
function Map:updateDebugOverlayParcels()
for x = 1, self.width do
for y = 1, self.height do
local xy = (y - 1) * self.width + x - 1
self.debug_text[xy] = self.th:getCellFlags(x, y).parcelId
if self.debug_text[xy] == 0 then
self.debug_text[xy] = ''
end
end
end
end
function Map:updateDebugOverlayCamera()
for x = 1, self.width do
for y = 1, self.height do
local xy = (y - 1) * self.width + x - 1
self.debug_text[xy] = ''
end
end
for p = 1, self.th:getPlayerCount() do
local x, y = self.th:getCameraTile(p)
if x and y then
local xy = (y - 1) * self.width + x - 1
self.debug_text[xy] = 'C'..p
end
end
end
function Map:updateDebugOverlayHeliport()
for x = 1, self.width do
for y = 1, self.height do
local xy = (y - 1) * self.width + x - 1
self.debug_text[xy] = ''
end
end
for p = 1, self.th:getPlayerCount() do
local x, y = self.th:getHeliportTile(p)
if x and y then
local xy = (y - 1) * self.width + x - 1
self.debug_text[xy] = 'H'..p
end
end
end
function Map:loadDebugText(base_offset, xy_offset, first, last, bits_)
self.debug_text = false
self.debug_flags = false
@@ -396,6 +476,18 @@ function Map:loadDebugText(base_offset, xy_offset, first, last, bits_)
self.debug_text = {}
self.updateDebugOverlay = self.updateDebugOverlayHeat
self:updateDebugOverlay()
elseif base_offset == "parcel" then
self.debug_text = {}
self.updateDebugOverlay = self.updateDebugOverlayParcels
self:updateDebugOverlay()
elseif base_offset == "camera" then
self.debug_text = {}
self.updateDebugOverlay = self.updateDebugOverlayCamera
self:updateDebugOverlay()
elseif base_offset == "heliport" then
self.debug_text = {}
self.updateDebugOverlay = self.updateDebugOverlayHeliport
self:updateDebugOverlay()
else
local thData = self:getRawData()
for x = 1, self.width do

View File

@@ -42,6 +42,8 @@ local Helicopter = _G["Helicopter"]
function Helicopter:Helicopter(world, object_type, hospital, direction, etc)
local x, y = hospital:getHeliportPosition()
-- Helicoptor needs to land below tile to be positioned correctly
y = y + 1
self:Object(world, object_type, x, y, direction, etc)
self.th:makeInvisible()
self:setPosition(0, -600)

View File

@@ -262,7 +262,7 @@ function World:initLevel(app, avail_rooms)
end
end
end
if #self.available_diseases == 0 and not _MAP_EDITOR then
if #self.available_diseases == 0 and not self.map.level_number == "MAP EDITOR" then
-- No diseases are needed if we're actually in the map editor!
print("Warning: This level does not contain any diseases")
end
@@ -975,6 +975,8 @@ local outside_temperatures = {
--! World ticks are translated to game ticks (or hours) depending on the
-- current speed of the game. There are 50 hours in a TH day.
function World:onTick()
if self.map.level_number == "MAP EDITOR" then return end
if self.tick_timer == 0 then
if self.autosave_next_tick then
self.autosave_next_tick = nil

View File

@@ -24,6 +24,10 @@ SOFTWARE.
#include "th_map.h"
#include "th_pathfind.h"
#include <cstring>
#include <string>
#include <exception>
static const int player_max = 4;
static int l_map_new(lua_State *L)
{
@@ -108,6 +112,14 @@ static int l_map_loadblank(lua_State *L)
return 2;
}
static int l_map_save(lua_State *L)
{
THMap *pMap = luaT_testuserdata<THMap>(L);
std::string filename(luaL_checkstring(L, 2));
pMap->save(filename);
return 0;
}
static THAnimation* l_map_updateblueprint_getnextanim(lua_State *L, int& iIndex)
{
THAnimation *pAnim;
@@ -344,6 +356,22 @@ static int l_map_get_player_count(lua_State *L)
return 1;
}
static int l_map_set_player_count(lua_State *L)
{
THMap* pMap = luaT_testuserdata<THMap>(L);
int count = static_cast<int>(luaL_checkinteger(L, 2));
try
{
pMap->setPlayerCount(count);
}
catch (std::out_of_range)
{
return luaL_error(L, "Player count out of range %d", count);
}
return 0;
}
static int l_map_get_player_camera(lua_State *L)
{
THMap* pMap = luaT_testuserdata<THMap>(L);
@@ -357,6 +385,20 @@ static int l_map_get_player_camera(lua_State *L)
return 2;
}
static int l_map_set_player_camera(lua_State *L)
{
THMap* pMap = luaT_testuserdata<THMap>(L);
int iX = static_cast<int>(luaL_checkinteger(L, 2) - 1);
int iY = static_cast<int>(luaL_checkinteger(L, 3) - 1);
int iPlayer = static_cast<int>(luaL_optinteger(L, 4, 1));
if (iPlayer < 1 || iPlayer > player_max)
return luaL_error(L, "Player index out of range: %i", iPlayer);
pMap->setPlayerCameraTile(iPlayer - 1, iX, iY);
return 0;
}
static int l_map_get_player_heliport(lua_State *L)
{
THMap* pMap = luaT_testuserdata<THMap>(L);
@@ -365,12 +407,26 @@ static int l_map_get_player_heliport(lua_State *L)
bool bGood = pMap->getPlayerHeliportTile(iPlayer - 1, &iX, &iY);
bGood = pMap->getPlayerHeliportTile(iPlayer - 1, &iX, &iY);
if(!bGood)
return luaL_error(L, "Player index out of range: %i", iPlayer);
return luaL_error(L, "Player index out of range: %d", iPlayer);
lua_pushinteger(L, iX + 1);
lua_pushinteger(L, iY + 2);
lua_pushinteger(L, iY + 1);
return 2;
}
static int l_map_set_player_heliport(lua_State *L)
{
THMap* pMap = luaT_testuserdata<THMap>(L);
int iX = static_cast<int>(luaL_checkinteger(L, 2) - 1);
int iY = static_cast<int>(luaL_checkinteger(L, 3) - 1);
int iPlayer = static_cast<int>(luaL_optinteger(L, 4, 1));
if (iPlayer < 1 || iPlayer > player_max)
return luaL_error(L, "Player index out of range: %i", iPlayer);
pMap->setPlayerHeliportTile(iPlayer - 1, iX, iY);
return 0;
}
static int l_map_getcell(lua_State *L)
{
THMap* pMap = luaT_testuserdata<THMap>(L);
@@ -855,10 +911,14 @@ void THLuaRegisterMap(const THLuaRegisterState_t *pState)
luaT_setmetamethod(l_map_depersist, "depersist", MT_Anim);
luaT_setfunction(l_map_load, "load");
luaT_setfunction(l_map_loadblank, "loadBlank");
luaT_setfunction(l_map_save, "save");
luaT_setfunction(l_map_getsize, "size");
luaT_setfunction(l_map_get_player_count, "getPlayerCount");
luaT_setfunction(l_map_set_player_count, "setPlayerCount");
luaT_setfunction(l_map_get_player_camera, "getCameraTile");
luaT_setfunction(l_map_set_player_camera, "setCameraTile");
luaT_setfunction(l_map_get_player_heliport, "getHeliportTile");
luaT_setfunction(l_map_set_player_heliport, "setHeliportTile");
luaT_setfunction(l_map_getcell, "getCell");
luaT_setfunction(l_map_gettemperature, "getCellTemperature");
luaT_setfunction(l_map_getcellflags, "getCellFlags");

View File

@@ -30,6 +30,9 @@ SOFTWARE.
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <fstream>
#include <string>
#include <exception>
th_map_node_flags& th_map_node_flags::operator=(uint32_t raw)
{
@@ -485,16 +488,16 @@ bool THMap::loadFromTHFile(const uint8_t* pData, size_t iDataLength,
return true;
}
void THMap::save(void (*fnWriter)(void*, const uint8_t*, size_t),
void* pToken)
void THMap::save(std::string filename)
{
uint8_t aBuffer[256] = {0};
int iBufferNext = 0;
std::basic_ofstream<uint8_t, std::char_traits<uint8_t>> os(filename, std::ios_base::trunc | std::ios_base::binary);
// Header
aBuffer[0] = static_cast<uint8_t>(m_iPlayerCount);
// TODO: Determine correct contents for the next 33 bytes
fnWriter(pToken, aBuffer, 34);
os.write(aBuffer, 34);
uint8_t aReverseBlockLUT[256] = {0};
for(int i = 0; i < 256; ++i)
@@ -540,7 +543,7 @@ void THMap::save(void (*fnWriter)(void*, const uint8_t*, size_t),
if(iBufferNext == sizeof(aBuffer))
{
fnWriter(pToken, aBuffer, sizeof(aBuffer));
os.write(aBuffer, sizeof(aBuffer));
iBufferNext = 0;
}
}
@@ -551,7 +554,7 @@ void THMap::save(void (*fnWriter)(void*, const uint8_t*, size_t),
aBuffer[iBufferNext++] = static_cast<uint8_t>(pNode->iParcelId >> 8);
if(iBufferNext == sizeof(aBuffer))
{
fnWriter(pToken, aBuffer, sizeof(aBuffer));
os.write(aBuffer, sizeof(aBuffer));
iBufferNext = 0;
}
}
@@ -559,7 +562,7 @@ void THMap::save(void (*fnWriter)(void*, const uint8_t*, size_t),
// TODO: What are these two bytes?
aBuffer[iBufferNext++] = 3;
aBuffer[iBufferNext++] = 0;
fnWriter(pToken, aBuffer, iBufferNext);
os.write(aBuffer, iBufferNext);
iBufferNext = 0;
std::memset(aBuffer, 0, 56);
@@ -571,10 +574,11 @@ void THMap::save(void (*fnWriter)(void*, const uint8_t*, size_t),
m_aiHeliportX[i], m_aiHeliportY[i]);
iBufferNext += 2;
}
fnWriter(pToken, aBuffer, 16);
os.write(aBuffer, 16);
std::memset(aBuffer, 0, 16);
// TODO: What are these 56 bytes?
fnWriter(pToken, aBuffer, 56);
os.write(aBuffer, 56);
os.close();
}
void THMap::setParcelOwner(int iParcelId, int iOwner)
@@ -733,6 +737,14 @@ bool THMap::isParcelPurchasable(int iParcelId, int iPlayer)
return false;
}
void THMap::setPlayerCount(int count)
{
if (count < 1 || count > 4)
throw new std::out_of_range("Player count must be between 1 and 4");
m_iPlayerCount = count;
}
bool THMap::getPlayerCameraTile(int iPlayer, int* pX, int* pY) const
{
if(iPlayer < 0 || iPlayer >= getPlayerCount())

View File

@@ -24,6 +24,7 @@ SOFTWARE.
#define CORSIX_TH_TH_MAP_H_
#include "th_gfx.h"
#include <list>
#include <string>
/*
Object type enumeration uses same values as original TH does.
@@ -241,8 +242,7 @@ public:
THMapLoadObjectCallback_t fnObjectCallback,
void* pCallbackToken);
void save(void (*fnWriter)(void*, const uint8_t*, size_t),
void* pToken);
void save(std::string filename);
//! Set the sprite sheet to be used for drawing the map
/*!
@@ -275,6 +275,9 @@ public:
inline int getParcelCount() const {return m_iParcelCount - 1;}
inline int getPlayerCount() const {return m_iPlayerCount;}
void setPlayerCount(int count);
bool getPlayerCameraTile(int iPlayer, int* pX, int* pY) const;
bool getPlayerHeliportTile(int iPlayer, int* pX, int* pY) const;
void setPlayerCameraTile(int iPlayer, int iX, int iY);