1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-01-05 22:53:39 +02:00

Merge remote-tracking branch 'upstream/develop' into diggy_cutscene

This commit is contained in:
SimonFlapse 2019-07-07 20:20:05 +02:00
commit 2b9ca4013a
10 changed files with 736 additions and 84 deletions

View File

@ -362,7 +362,9 @@ global.config = {
},
-- enables a command which allows for an end-game event
apocalypse = {
enabled = true
enabled = true,
-- chance behemoth biters and spitters will double on death.
duplicate_chance = 0.05
},
-- gradually informs players of features such as chat, toasts, etc.
player_onboarding = {

View File

@ -151,6 +151,8 @@ if config.redmew_settings.enabled then
require 'features.gui.redmew_settings'
end
require 'features.snake.control'
-- Debug-only modules
if _DEBUG then
require 'features.scenario_data_manipulation'

View File

@ -5,15 +5,13 @@ local Global = require 'utils.global'
local Command = require 'utils.command'
local Toast = require 'features.gui.toast'
local RS = require 'map_gen.shared.redmew_surface'
local HailHydra = require 'map_gen.shared.hail_hydra'
local Color = require 'resources.color_presets'
local Ranks = require 'resources.ranks'
local Event = require 'utils.event'
local random = math.random
local Config = require 'config'
-- Constants
local hail_hydra_data = {
['behemoth-spitter'] = {['behemoth-spitter'] = 0.01},
['behemoth-biter'] = {['behemoth-biter'] = 0.01}
}
local duplicate_chance = Config.apocalypse.duplicate_chance
-- Local var
local Public = {}
@ -35,6 +33,54 @@ Global.register(
end
)
local name_map = {['behemoth-biter'] = true, ['behemoth-spitter'] = true}
local biter_died_token =
Token.register(
function(event)
local entity = event.entity
if not entity.valid then
return
end
local name = entity.name
if not name_map[name] then
return
end
local force_name = entity.force.name
if force_name ~= 'enemy' then
return
end
local surface = entity.surface
local create_entity = surface.create_entity
local position = entity.position
local spawn = {name = entity.name, force = 'enemy', position = position}
create_entity(spawn)
if random() > duplicate_chance then
return
end
local spawn_position = surface.find_non_colliding_position(name, position, 8, 1)
if not spawn_position then
return
end
spawn.position = spawn_position
create_entity(spawn)
end
)
local aliens = {
'behemoth-biter',
'behemoth-biter',
'behemoth-spitter',
'behemoth-spitter'
}
local biter_spawn_token =
Token.register(
function()
@ -45,9 +91,6 @@ local biter_spawn_token =
surface = RS.get_surface()
player_force = game.forces.player
HailHydra.set_hydras(hail_hydra_data)
HailHydra.set_evolution_scale(1)
HailHydra.enable_hail_hydra()
enemy_force.evolution_factor = 1
local p_spawn = player_force.get_spawn_position(surface)
@ -55,13 +98,6 @@ local biter_spawn_token =
local create_entity = surface.create_entity
local aliens = {
'behemoth-biter',
'behemoth-biter',
'behemoth-spitter',
'behemoth-spitter'
}
for i = 1, #aliens do
local spawn_pos = surface.find_non_colliding_position('behemoth-biter', p_spawn, 300, 1)
if spawn_pos then
@ -72,6 +108,8 @@ local biter_spawn_token =
group.set_command({type = defines.command.attack_area, destination = {0, 0}, radius = 500})
Toast.toast_all_players(500, {'apocalypse.toast_message'})
Event.add_removable(defines.events.on_entity_died, biter_died_token)
end
)
@ -97,7 +135,7 @@ function Public.begin_apocalypse(_, player)
primitives.apocalypse_now = true
game.print({'apocalypse.apocalypse_begins'}, Color.pink)
Task.set_timeout(15, biter_spawn_token, {})
Task.set_timeout(1, biter_spawn_token, {})
end
Command.add(

View File

@ -0,0 +1,28 @@
local GameGui = require 'features.snake.gui'
local Game = require 'features.snake.game'
local Public = {}
--- Starts snake game.
-- Note when players join the game they will lose thier character.
-- @param surface <LuaSurface> Surface that the board is placed on.
-- @param top_left_position <Position> Position where board is placed. Defaults to {x = 1, y = 1}.
-- @param size <int> size of board in board tiles. Note that the actual size of the board will be (2 * size) + 1 in
-- factorio tiles. Defaults to 15.
-- @param update_rate <int> number of ticks between updates. Defaults to 30.
-- @param <int> maximun food on the board. Defaults to 6.
function Public.start_game(surface, top_left_position, size, update_rate, max_food)
Game.start_game(surface, top_left_position, size, update_rate, max_food)
GameGui.show()
end
--- Ends the snake game. This will clean up any snake and food entities but will not restore the tiles nor
-- give players thier character back.
function Public.end_game()
Game.end_game()
GameGui.destroy()
end
remote.add_interface('snake', Public)
return Public

411
features/snake/game.lua Normal file
View File

@ -0,0 +1,411 @@
local Global = require 'utils.global'
local Event = require 'utils.event'
local Token = require 'utils.token'
local Queue = require 'utils.queue'
local random = math.random
local queue_new = Queue.new
local push = Queue.push
local push_to_end = Queue.push_to_end
local pop = Queue.pop
local peek = Queue.peek
local peek_start = Queue.peek_start
local peek_index = Queue.peek_index
local queue_size = Queue.size
local queue_pairs = Queue.pairs
local pairs = pairs
local snakes = {} -- player_index -> snake_data {is_marked_for_destroy:bool, queue :Queue of {entity, cord} }
local board = {
size = 0,
surface = nil,
position = nil,
food_count = 0,
is_running = false,
update_rate = 30,
max_food = 6
}
local cords_map = {} -- cords -> positions
Global.register(
{snakes = snakes, board = board, cords_map = cords_map},
function(tbl)
snakes = tbl.snakes
board = tbl.board
cords_map = tbl.cords_map
end
)
local vectors = {
[0] = {x = 0, y = -1},
[1] = {x = 0, y = -1},
[2] = {x = 1, y = 0},
[3] = {x = 1, y = 0},
[4] = {x = 0, y = 1},
[5] = {x = 0, y = 1},
[6] = {x = -1, y = 0},
[7] = {x = -1, y = 0}
}
local function destroy_snake(index, snake)
for _, element in queue_pairs(snake.queue) do
local e = element.entity
if e and e.valid then
e.destroy()
end
end
snakes[index] = nil
local player = game.get_player(index)
if not player or not player.valid then
return
end
game.print({'snake.snake_destroyed', player.name, queue_size(snake.queue)})
end
local function destroy_dead_snakes()
for index, snake in pairs(snakes) do
if snake.is_marked_for_destroy then
destroy_snake(index, snake)
end
end
end
local function spawn_food()
local size = board.size
local center = math.ceil(size / 2)
local surface = board.surface
local find_entity = surface.find_entity
local food_count = board.food_count
local max_food = board.max_food
local tries = max_food - food_count + 10
while food_count < max_food and tries > 0 do
while tries > 0 do
tries = tries - 1
local x, y = random(size), random(size)
if x == center and y == center then
goto continue
end
local pos = cords_map[x][y]
local entity = find_entity('character', pos) or find_entity('compilatron', pos)
if entity then
goto continue
end
entity =
surface.create_entity({name = 'compilatron', position = pos, force = 'neutral', direction = random(7)})
entity.active = false
food_count = food_count + 1
break
::continue::
end
end
board.food_count = food_count
end
local function destroy_food()
local position = board.position
local size = board.size
local food =
board.surface.find_entities_filtered(
{
name = 'compilatron',
area = {left_top = position, right_bottom = {position.x + size * 2, position.y + size * 2}}
}
)
for i = 1, #food do
local e = food[i]
if e.valid then
e.destroy()
end
end
board.food_count = 0
end
local function get_new_head_cord(head_cord, direction)
local vector = vectors[direction]
local vec_x, vec_y = vector.x, vector.y
local x, y = head_cord.x + vec_x, head_cord.y + vec_y
return x, y
end
local function tick_snake(index, snake)
local player = game.get_player(index)
if not player or not player.valid then
snake.is_marked_for_destroy = true
return
end
local character = player.character
if not character or not character.valid then
snake.is_marked_for_destroy = true
return
end
local surface = board.surface
local find_entity = surface.find_entity
local snake_queue = snake.queue
local snake_size = queue_size(snake_queue)
local head = peek_start(snake_queue)
local tail = peek(snake_queue)
local head_cord = head.cord
local tail_entity = tail.entity
local tail_cord = tail.cord
local size = board.size
local walking_state = character.walking_state
walking_state.walking = true
local direction = walking_state.direction
local x, y = get_new_head_cord(head_cord, direction)
if x <= 0 or x > size or y <= 0 or y > size then
snake.is_marked_for_destroy = true
tail_entity.destroy()
return
end
local new_head_position = cords_map[x][y]
if snake_size > 1 and find_entity('character', new_head_position) == peek_index(snake_queue, snake_size - 1).entity then
direction = (direction + 4) % 8
walking_state.direction = direction
x, y = get_new_head_cord(head_cord, direction)
end
if x <= 0 or x > size or y <= 0 or y > size then
snake.is_marked_for_destroy = true
tail_entity.destroy()
return
end
new_head_position = cords_map[x][y]
tail_entity.teleport(new_head_position)
tail.cord = {x = x, y = y}
pop(snake_queue)
push(snake_queue, tail)
player.character = nil
player.character = tail_entity
tail_entity.walking_state = walking_state
head.entity.active = false
tail_entity.active = true
local entity = find_entity('compilatron', new_head_position)
if entity and entity.valid then
entity.destroy()
entity =
surface.create_entity {name = 'character', position = cords_map[tail_cord.x][tail_cord.y], force = 'player'}
entity.character_running_speed_modifier = -1
entity.color = player.color
entity.active = false
push_to_end(snake_queue, {entity = entity, cord = tail_cord})
board.food_count = board.food_count - 1
end
end
local function tick_snakes()
for index, snake in pairs(snakes) do
tick_snake(index, snake)
end
end
local function check_snakes_for_collisions()
local count_entities_filtered = board.surface.count_entities_filtered
for index, snake in pairs(snakes) do
if snake.is_marked_for_destroy then
goto continue
end
if count_entities_filtered({name = 'character', position = peek_start(snake.queue).entity.position}) > 1 then
snake.is_marked_for_destroy = true
end
::continue::
end
end
local tick =
Token.register(
function()
tick_snakes()
check_snakes_for_collisions()
destroy_dead_snakes()
spawn_food()
end
)
local function make_board()
local size = board.size
local position = board.position
local surface = board.surface
local pos_x, pos_y = position.x, position.y
for x = 1, size do
local col = {}
cords_map[x] = col
for y = 1, size do
col[y] = {pos_x + 2 * x - 0.5, pos_y + 2 * y - 0.5}
end
end
size = size * 2
local tiles = {}
for x = 0, size do
for y = 0, size do
local pos = {pos_x + x, pos_y + y}
local tile_name
if x == 0 or x == size or y == 0 or y == size then
tile_name = 'deepwater'
elseif x % 2 == 1 and y % 2 == 1 then
tile_name = 'grass-1'
else
tile_name = 'water'
end
tiles[#tiles + 1] = {position = pos, name = tile_name}
end
end
surface.set_tiles(tiles)
end
local function find_new_snake_position()
local size = board.size
local find_entity = board.surface.find_entity
local min = math.min(4, size)
local max = math.max(1, size - 4)
if min > max then
min, max = max, min
elseif min == max then
min = 1
max = size
end
local tries = 10
while tries > 0 do
tries = tries - 1
local x, y = random(min, max), random(min, max)
local pos = cords_map[x][y]
local entity = find_entity('character', pos) or find_entity('compilatron', pos)
if not entity then
return {x = x, y = y}, pos
end
end
end
local function new_snake(player)
if not board.is_running then
return
end
if not player or not player.valid then
return
end
if snakes[player.index] then
return
end
local character = player.character
if character and character.valid then
character.destroy()
end
local cord, pos = find_new_snake_position()
if not cord then
player.print({'snake.spawn_snake_fail'})
return
end
player.teleport(pos, board.surface)
player.create_character()
character = player.character
character.character_running_speed_modifier = -1
local queue = queue_new()
push(queue, {entity = character, cord = cord})
local snake = {queue = queue}
snakes[player.index] = snake
end
local function new_game(surface, position, size, update_rate, max_food)
board.size = size or 15
board.surface = surface
position = position or {x = 1, y = 1}
position.x = position.x or position[1]
position.y = position.y or position[2]
board.position = position
board.update_rate = update_rate or 30
board.max_food = max_food or 6
make_board()
destroy_food()
spawn_food()
board.is_running = true
Event.add_removable_nth_tick(board.update_rate, tick)
end
local Public = {}
function Public.start_game(surface, top_left_position, size, update_rate, max_food)
if board.is_running then
error('Snake game is already running you must end the game first.', 2)
end
if not surface then
error('Surface must be set.', 2)
end
new_game(surface, top_left_position, size, update_rate or board.update_rate, max_food or board.max_food)
end
function Public.end_game()
for index, snake in pairs(snakes) do
destroy_snake(index, snake)
end
destroy_food()
Event.remove_removable_nth_tick(board.update_rate, tick)
board.is_running = false
end
function Public.new_snake(player)
new_snake(player)
end
function Public.is_running()
return board.is_running
end
return Public

51
features/snake/gui.lua Normal file
View File

@ -0,0 +1,51 @@
local Game = require 'features.snake.game'
local Gui = require 'utils.gui'
local Event = require 'utils.event'
local Public = {}
local main_button_name = Gui.uid_name()
local function show_gui_for_player(player)
if not player or not player.valid then
return
end
local top = player.gui.top
if not top[main_button_name] then
top.add {type = 'button', name = main_button_name, caption = {'snake.name'}}
end
end
local function player_created(event)
if Game.is_running() then
local player = game.get_player(event.player_index)
show_gui_for_player(player)
end
end
Event.add(defines.events.on_player_created, player_created)
function Public.show()
for _, player in pairs(game.players) do
show_gui_for_player(player)
end
end
function Public.destroy()
for _, player in pairs(game.players) do
local button = player.gui.top[main_button_name]
if button and button.valid then
button.destroy()
end
end
end
Gui.on_click(
main_button_name,
function(event)
Game.new_snake(event.player)
end
)
return Public

View File

@ -161,3 +161,8 @@ ammo_count=Autofill ammo count
invalid_ammo_count=ammo count must be a positive integer
main_button_tooltip=Autofill settings
frame_name=Autofill
[snake]
name=Snake
spawn_snake_fail=Unable to spawn snake, please try again.
snake_destroyed=__1__ has been destroyed with a score of __2__.

View File

@ -1,20 +1,46 @@
local Debug = require 'utils.debug'
local is_closure = Debug.is_closure
local floor = math.floor
local PriorityQueue = {}
function PriorityQueue.new()
return {}
end
local function default_comp(a, b)
local function default_comparator(a, b)
return a < b
end
local function HeapifyFromEndToStart(queue, comp)
comp = comp or default_comp
local pos = #queue
--- Min heap implementation of a priority queue. Smaller elements, as determined by the comparator,
-- have a higher priority.
-- @param comparator <function|nil> the comparator function used to compare elements, if nil the
-- deafult comparator is used.
-- @usage
-- local PriorityQueue = require 'utils.priority_queue'
--
-- local queue = PriorityQueue.new()
-- PriorityQueue.push(queue, 4)
-- PriorityQueue.push(queue, 7)
-- PriorityQueue.push(queue, 2)
--
-- game.print(PriorityQueue.pop(queue)) -- 2
-- game.print(PriorityQueue.pop(queue)) -- 4
-- game.print(PriorityQueue.pop(queue)) -- 7
function PriorityQueue.new(comparator)
if comparator == nil then
comparator = default_comparator
elseif is_closure(comparator) then
error('comparator cannot be a closure.', 2)
end
return {_comparator = comparator}
end
local function heapify_from_end_to_start(self)
local comparator = self._comparator
local pos = #self
while pos > 1 do
local parent = bit32.rshift(pos, 1) -- integer division by 2
if comp(queue[pos], queue[parent]) then
queue[pos], queue[parent] = queue[parent], queue[pos]
local parent = floor(pos * 0.5)
local a, b = self[pos], self[parent]
if comparator(a, b) then
self[pos], self[parent] = b, a
pos = parent
else
break
@ -22,25 +48,26 @@ local function HeapifyFromEndToStart(queue, comp)
end
end
local function HeapifyFromStartToEnd(queue, comp)
comp = comp or default_comp
local function heapify_from_start_to_end(self)
local comparator = self._comparator
local parent = 1
local smallest = 1
local count = #self
while true do
local child = parent * 2
if child > #queue then
if child > count then
break
end
if comp(queue[child], queue[parent]) then
if comparator(self[child], self[parent]) then
smallest = child
end
child = child + 1
if child <= #queue and comp(queue[child], queue[smallest]) then
if child <= count and comparator(self[child], self[smallest]) then
smallest = child
end
if parent ~= smallest then
queue[parent], queue[smallest] = queue[smallest], queue[parent]
self[parent], self[smallest] = self[smallest], self[parent]
parent = smallest
else
break
@ -48,27 +75,33 @@ local function HeapifyFromStartToEnd(queue, comp)
end
end
function PriorityQueue.size(queue)
return #queue
--- Returns the number of the number of elements in the priority queue.
function PriorityQueue.size(self)
return #self
end
function PriorityQueue.push(queue, element, comp)
table.insert(queue, element)
HeapifyFromEndToStart(queue, comp)
-- Inserts an element into the priority queue.
function PriorityQueue.push(self, element)
self[#self + 1] = element
heapify_from_end_to_start(self)
end
function PriorityQueue.pop(queue, comp)
local element = queue[1]
-- Removes and returns the highest priority element from the priority queue.
-- If the priority queue is empty returns nil.
function PriorityQueue.pop(self)
local element = self[1]
queue[1] = queue[#queue]
queue[#queue] = nil
HeapifyFromStartToEnd(queue, comp)
self[1] = self[#self]
self[#self] = nil
heapify_from_start_to_end(self)
return element
end
function PriorityQueue.peek(queue)
return queue[1]
-- Returns, without removing, the highest priority element from the priority queue.
-- If the priority queue is empty returns nil.
function PriorityQueue.peek(self)
return self[1]
end
return PriorityQueue

View File

@ -1,24 +1,39 @@
local Queue = {}
function Queue.new()
local queue = {_head = 0, _tail = 0}
local queue = {_head = 1, _tail = 1}
return queue
end
function Queue.size(queue)
return queue._tail - queue._head
return queue._head - queue._tail
end
function Queue.push(queue, element)
local index = queue._head
queue[index] = element
queue._head = index - 1
queue._head = index + 1
end
--- Pushes the element such that it would be the next element pop'ed.
function Queue.push_to_end(queue, element)
local index = queue._tail - 1
queue[index] = element
queue._tail = index
end
function Queue.peek(queue)
return queue[queue._tail]
end
function Queue.peek_start(queue)
return queue[queue._head - 1]
end
function Queue.peek_index(queue, index)
return queue[queue._tail + index - 1]
end
function Queue.pop(queue)
local index = queue._tail
@ -26,9 +41,37 @@ function Queue.pop(queue)
queue[index] = nil
if element then
queue._tail = index - 1
queue._tail = index + 1
end
return element
end
function Queue.to_array(queue)
local n = 1
local res = {}
for i = queue._tail, queue._head - 1 do
res[n] = queue[i]
n = n + 1
end
return res
end
function Queue.pairs(queue)
local index = queue._tail
return function()
local element = queue[index]
if element then
local old = index
index = index + 1
return old, element
else
return nil
end
end
end
return Queue

View File

@ -9,38 +9,65 @@ local PriorityQueue = require 'utils.priority_queue'
local Event = require 'utils.event'
local Token = require 'utils.token'
local ErrorLogging = require 'utils.error_logging'
local Global = require 'utils.global'
local floor = math.floor
local log10 = math.log10
local Token_get = Token.get
local pcall = pcall
local Queue_peek = Queue.peek
local Queue_pop = Queue.pop
local Queue_push = Queue.push
local PriorityQueue_peek = PriorityQueue.peek
local PriorityQueue_pop = PriorityQueue.pop
local PriorityQueue_push = PriorityQueue.push
local Task = {}
global.callbacks = global.callbacks or PriorityQueue.new()
global.next_async_callback_time = -1
global.task_queue = global.task_queue or Queue.new()
global.total_task_weight = 0
global.task_queue_speed = 1
local function comp(a, b)
local function comparator(a, b)
return a.time < b.time
end
global.tpt = global.task_queue_speed
local function get_task_per_tick()
if game.tick % 300 == 0 then
local size = global.total_task_weight
global.tpt = math.floor(math.log10(size + 1)) * global.task_queue_speed
if global.tpt < 1 then
global.tpt = 1
end
local callbacks = PriorityQueue.new(comparator)
local task_queue = Queue.new()
local primitives = {
next_async_callback_time = -1,
total_task_weight = 0,
task_queue_speed = 1,
task_per_tick = 1
}
Global.register(
{callbacks = callbacks, task_queue = task_queue, primitives = primitives},
function(tbl)
callbacks = tbl.callbacks
task_queue = tbl.task_queue
primitives = tbl.primitives
end
return global.tpt
)
local function get_task_per_tick(tick)
if tick % 300 == 0 then
local size = primitives.total_task_weight
local task_per_tick = floor(log10(size + 1)) * primitives.task_queue_speed
if task_per_tick < 1 then
task_per_tick = 1
end
primitives.task_per_tick = task_per_tick
return task_per_tick
end
return primitives.task_per_tick
end
local function on_tick()
local queue = global.task_queue
for i = 1, get_task_per_tick() do
local task = Queue.peek(queue)
local tick = game.tick
for i = 1, get_task_per_tick(tick) do
local task = Queue_peek(task_queue)
if task ~= nil then
-- result is error if not success else result is a boolean for if the task should stay in the queue.
local success, result = pcall(Token.get(task.func_token), task.params)
local success, result = pcall(Token_get(task.func_token), task.params)
if not success then
if _DEBUG then
error(result)
@ -48,19 +75,18 @@ local function on_tick()
log(result)
ErrorLogging.generate_error_report(result)
end
Queue.pop(queue)
global.total_task_weight = global.total_task_weight - task.weight
Queue_pop(task_queue)
primitives.total_task_weight = primitives.total_task_weight - task.weight
elseif not result then
Queue.pop(queue)
global.total_task_weight = global.total_task_weight - task.weight
Queue_pop(task_queue)
primitives.total_task_weight = primitives.total_task_weight - task.weight
end
end
end
local callbacks = global.callbacks
local callback = PriorityQueue.peek(callbacks)
while callback ~= nil and game.tick >= callback.time do
local success, result = pcall(Token.get(callback.func_token), callback.params)
local callback = PriorityQueue_peek(callbacks)
while callback ~= nil and tick >= callback.time do
local success, result = pcall(Token_get(callback.func_token), callback.params)
if not success then
if _DEBUG then
error(result)
@ -69,8 +95,8 @@ local function on_tick()
ErrorLogging.generate_error_report(result)
end
end
PriorityQueue.pop(callbacks, comp)
callback = PriorityQueue.peek(callbacks)
PriorityQueue_pop(callbacks)
callback = PriorityQueue_peek(callbacks)
end
end
@ -85,7 +111,7 @@ function Task.set_timeout_in_ticks(ticks, func_token, params)
end
local time = game.tick + ticks
local callback = {time = time, func_token = func_token, params = params}
PriorityQueue.push(global.callbacks, callback, comp)
PriorityQueue_push(callbacks, callback)
end
--- Allows you to set a timer (in seconds) after which the tokened function will be run with params given as an argument
@ -109,8 +135,21 @@ end
-- Ex. if the task is expected to repeat multiple times (ie. the function returns true and loops several ticks)
function Task.queue_task(func_token, params, weight)
weight = weight or 1
global.total_task_weight = global.total_task_weight + weight
Queue.push(global.task_queue, {func_token = func_token, params = params, weight = weight})
primitives.total_task_weight = primitives.total_task_weight + weight
Queue_push(task_queue, {func_token = func_token, params = params, weight = weight})
end
function Task.get_queue_speed()
return primitives.task_queue_speed
end
function Task.set_queue_speed(value)
value = value or 1
if value < 0 then
value = 0
end
primitives.task_queue_speed = value
end
Event.add(defines.events.on_tick, on_tick)