1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-02-11 13:39:17 +02:00

Merge pull request #947 from SimonFlapse/diggy_cutscene

Cutscene Controller
This commit is contained in:
theorangeangle 2019-08-31 20:52:28 -05:00 committed by GitHub
commit fc1d1af9d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1594 additions and 26 deletions

View File

@ -253,6 +253,12 @@ stds.factorio_control = {
'set_forces',
'get_players',
'set_players',
'get_visible',
'set_visible',
'get_draw_on_ground',
'set_draw_on_ground',
'get_only_in_alt_mode',
'set_only_in_alt_mode',
'get_color',
'set_color',
'get_width',

View File

@ -169,6 +169,7 @@ global.config = {
'Welcome to this map created by the RedMew team. You can join our discord at: redmew.com/discord',
'Click the question mark in the top left corner for server information and map details.'
},
cutscene = false,
-- format is a table: {{message, weight}, {message, weight}}, where a higher weight has more chance to be shown
random_join_message_set = require 'resources.join_messages',
-- applied when cheat_mode is set to true
@ -306,6 +307,10 @@ global.config = {
rich_text_gui = {
enabled = true
},
-- adds a command to open a gui that can play sounds from a list
radio = {
enabled = false
},
-- adds a camera to watch another player
camera = {
enabled = true

View File

@ -144,6 +144,9 @@ end
if config.rich_text_gui.enabled then
require 'features.gui.rich_text'
end
if config.radio.enabled or _DEBUG then
require 'features.gui.radio'
end
if config.redmew_settings.enabled then
require 'features.gui.redmew_settings'
end

View File

@ -0,0 +1,529 @@
local Event = require 'utils.event'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Global = require 'utils.global'
local Command = require 'utils.command'
local Debug = require 'utils.debug'
local Gui = require 'utils.gui'
local set_timeout_in_ticks = Task.set_timeout_in_ticks
local debug_print = Debug.print
local skip_btn_name = Gui.uid_name()
local backward_btn_name = Gui.uid_name()
local forward_btn_name = Gui.uid_name()
local Public = {}
local handler
local cutscene_functions = {}
local running_cutscenes = {}
local replay = {
identifier = nil,
final_transition_time = nil
}
Global.register(
{
cutscene_functions = cutscene_functions,
running_cutscenes = running_cutscenes,
replay = replay
},
function(tbl)
cutscene_functions = tbl.cutscene_functions
running_cutscenes = tbl.running_cutscenes
replay = tbl.replay
end
)
local function valid(entity)
return entity and entity.valid
end
local function waypoint_still_active(tick, player_index)
local running_cutscene = running_cutscenes[player_index]
tick = tick or -1
if tick == -1 then
debug_print('Tick was nil', 5)
end
if not running_cutscene or tick < running_cutscene.start_tick then
return false
end
return true
end
local toggle_gui_delayed =
Token.register(
function(params)
local player = params.player
if not waypoint_still_active(params.tick, player.index) then
debug_print('Cutscene is no longer active. Skipping toggle_gui')
return
end
local event = {player = player}
local clear = params.clear
if clear == 'left' then
player.gui.left.clear()
elseif clear == 'top' then
player.gui.top.clear()
elseif clear == 'center' then
player.gui.center.clear()
end
params.gui.toggle(event)
end
)
function Public.toggle_gui(tick, player, gui, initial_delay, clear)
--[[if type(gui) == 'table' then
debug_print('Provided GUI is invalid.')
return
end]]
set_timeout_in_ticks(initial_delay, toggle_gui_delayed, {tick = tick, player = player, gui = gui, clear = clear})
end
local play_sound_delayed =
Token.register(
function(params)
local player = params.player
if not waypoint_still_active(params.tick, player.index) then
debug_print('Cutscene is no longer active. Skipping play_sound')
return
end
player.play_sound {path = params.path}
end
)
function Public.play_sound(tick, player, path, times, delay, initial_delay)
if not game.is_valid_sound_path(path) then
debug_print('Provided SoundPath is invalid. Try opening /radio and browse for a valid path')
return
end
if not waypoint_still_active(tick, player.index) then
debug_print('Cutscene is no longer active. Skipping play_sound')
return
end
times = times or 1
if times == 1 and not delay and initial_delay then
delay = initial_delay
end
if times > 1 or delay then
delay = delay or 20
initial_delay = initial_delay or 0
for i = 1, times, 1 do
set_timeout_in_ticks(initial_delay + delay * i, play_sound_delayed, {tick = tick, player = player, path = path})
end
else
player.play_sound {path = path}
end
end
local remove_renderings =
Token.register(
function(renderings)
for _, v in pairs(renderings) do
if rendering.is_valid(v) then
rendering.destroy(v)
debug_print('Deleted rendering with id: ' .. v)
end
end
end
)
---Asserts if a given variable is of the expected type using type().
---
---@param expected_type string
---@param given any
---@param variable_reference_message string displayed when the expectation is not met
local function assert_type(expected_type, given, variable_reference_message, allow_nil)
local given_type = type(given)
if given_type ~= expected_type and (allow_nil and given_type ~= 'nil') then
error('Argument ' .. variable_reference_message .. " must be of type '" .. expected_type .. "', given '" .. given_type .. "'")
end
end
function Public.register_cutscene_function(identifier, waypoints, func, terminate_func)
assert_type('string', identifier, 'identifier of function cutscene_controller.register_cutscene_function')
assert_type('table', waypoints, 'waypoints of function cutscene_controller.register_cutscene_function')
assert_type('number', func, 'func of function cutscene_controller.register_cutscene_function')
assert_type('number', terminate_func, 'func of function cutscene_controller.register_cutscene_function', true)
cutscene_functions[identifier] = {func = func, waypoints = waypoints, update = false, terminate_func = terminate_func}
end
function Public.register_running_cutscene(player_index, identifier, final_transition_time)
assert_type('number', player_index, 'player_index of function cutscene_controller.register_running_cutscene')
assert_type('string', identifier, 'identifier of function cutscene_controller.register_running_cutscene')
assert_type('number', final_transition_time, 'identifier of function cutscene_controller.register_running_cutscene', true)
local player = game.get_player(player_index)
if not valid(player) then
return
end
local cutscene_function = cutscene_functions[identifier]
if not cutscene_function then
return
end
local waypoints = cutscene_function.waypoints
if not waypoints then
return
end
if running_cutscenes[player_index] then
player.print({'cutscene_controller.cannot_start_new'})
return
end
running_cutscenes[player_index] = {
func = cutscene_function.func,
waypoints = waypoints,
update = cutscene_function.update,
final_transition_time = final_transition_time,
character = player.character,
terminate_func = cutscene_function.terminate_func,
rendering = {},
current_index = -1,
start_tick = 0
}
local running_cutscene = running_cutscenes[player_index]
if player.controller_type == defines.controllers.cutscene then
debug_print('' .. player.name .. ' was already in another cutscene not controlled by this module. It has been stopped')
player.exit_cutscene()
end
player.set_controller {type = defines.controllers.ghost}
final_transition_time = final_transition_time >= 0 and final_transition_time or 60
running_cutscene.final_transition_time = final_transition_time
running_cutscene.identifier = identifier
player.set_controller {
type = defines.controllers.cutscene,
waypoints = waypoints,
final_transition_time = final_transition_time
}
local flow = player.gui.top.add {type = 'flow'}
running_cutscene.btn = flow
local btn = flow.add {type = 'sprite-button', name = skip_btn_name, caption = 'Skip cutscene'}
btn.style.minimal_height = 28
btn.style.minimal_width = 150
btn.style.font = 'default-large-bold'
btn.style.font_color = {r = 255, g = 215, b = 0}
local back_btn = flow.add {type = 'sprite-button', name = backward_btn_name, caption = 'Go back'}
back_btn.style.minimal_height = 28
back_btn.style.minimal_width = 100
back_btn.style.font = 'default-large-bold'
back_btn.style.font_color = {r = 255, g = 215, b = 0}
local forward_btn = flow.add {type = 'sprite-button', name = forward_btn_name, caption = 'Go forward'}
forward_btn.style.minimal_height = 28
forward_btn.style.minimal_width = 100
forward_btn.style.font = 'default-large-bold'
forward_btn.style.font_color = {r = 255, g = 215, b = 0}
handler({player_index = player_index, waypoint_index = -1, tick = game.tick})
end
local function restart_cutscene(player_index, waypoints, start_index)
local current_running = running_cutscenes[player_index]
local final_transition_time = current_running.final_transition_time
current_running.update = false
local character = current_running.character
if not character then
log('Player index: ' .. player_index .. ' managed to lose their character in a cutscene')
end
local end_waypoint = {
-- end waypoint
position = character.position,
transition_time = final_transition_time,
time_to_wait = 1,
zoom = 1,
terminate = true
}
table.insert(waypoints, end_waypoint)
running_cutscenes[player_index] = {
func = current_running.func,
waypoints = waypoints,
update = false,
final_transition_time = final_transition_time,
character = character,
terminate_func = current_running.terminate_func,
rendering = current_running.rendering,
btn = current_running.btn,
current_index = current_running.current_index,
start_tick = current_running.start_tick
}
debug_print('Updating cutscene for player_index ' .. player_index)
debug_print(running_cutscenes[player_index])
local player = game.get_player(player_index)
if not valid(player) then
return
end
if player.controller_type == defines.controllers.cutscene then
player.exit_cutscene()
player.set_controller {type = defines.controllers.ghost}
end
player.set_controller {
type = defines.controllers.cutscene,
waypoints = waypoints,
final_transition_time = final_transition_time
}
if start_index then
player.jump_to_cutscene_waypoint(start_index + 1)
else
start_index = -1
end
handler({player_index = player_index, waypoint_index = start_index, tick = game.tick})
end
function Public.inject_waypoint(player_index, waypoint, waypoint_index, override)
local running_cutscene = running_cutscenes[player_index]
if not running_cutscene then
return
end
local waypoints = running_cutscene.waypoints
if not waypoints then
return
end
local copy_waypoints = {}
for i = 1, #waypoints do
table.insert(copy_waypoints, waypoints[i])
end
if override then
copy_waypoints[waypoint_index] = waypoint
else
table.insert(copy_waypoints, waypoint_index, waypoint)
end
running_cutscene.update = copy_waypoints
end
local callback_function =
Token.register(
function(params)
local player_index = params.player_index
local func_params = params.params
if waypoint_still_active(func_params.tick, player_index) then
Token.get(params.func)(player_index, params.waypoint_index, func_params)
else
debug_print('Skipping callback function. Cutscene got terminated!')
end
end
)
local reconnect_character =
Token.register(
function(params)
local player_index = params.player_index
local player = game.get_player(player_index)
local running_cutscene = params.running_cutscene
local character = running_cutscene.character
local func = running_cutscene.terminate_func
if valid(player) and valid(character) then
player.exit_cutscene()
player.set_controller {type = defines.controllers.character, character = character}
if func then
Token.get(func)(player_index)
end
Token.get(remove_renderings)(running_cutscene.rendering)
running_cutscene.btn.destroy()
running_cutscenes[player_index] = nil
end
end
)
function Public.terminate_cutscene(player_index, ticks)
local running_cutscene = running_cutscenes[player_index]
if not running_cutscene then
return
end
ticks = ticks and ticks or 1
debug_print('Terminating cutscene in ' .. ticks .. ' Ticks')
set_timeout_in_ticks(
ticks,
reconnect_character,
{
player_index = player_index,
running_cutscene = running_cutscene
}
)
end
function Public.register_rendering_id(player_index, tick, render_id)
if type(render_id) ~= 'table' then
render_id = {render_id}
end
local running_cutscene = running_cutscenes[player_index]
for _, id in pairs(render_id) do
if rendering.is_valid(id) then
if not waypoint_still_active(tick, player_index) then
debug_print('The rendering with id ' .. id .. ' was not added. Destroying it instead')
rendering.destroy(id)
else
table.insert(running_cutscene.rendering, id)
end
end
end
end
function Public.register_replay(identifier, final_transition_time)
replay.identifier = identifier
replay.final_transition_time = final_transition_time
debug_print('Identifier ' .. identifier .. ' registered as replay cutscene')
end
handler = function(event)
local player_index = event.player_index
local waypoint_index = event.waypoint_index
local tick = event.tick
debug_print('Waypoint_index ' .. waypoint_index .. ' has finished at tick: ' .. tick)
local running_cutscene = running_cutscenes[player_index]
if not running_cutscene then
return
end
running_cutscene.current_index = waypoint_index + 1
running_cutscene.start_tick = tick
local update = running_cutscene.update
if update then
restart_cutscene(player_index, update, waypoint_index)
return
end
local ticks = running_cutscene.waypoints[waypoint_index + 2]
if ticks then
ticks = ticks.transition_time
else
ticks = running_cutscene.final_transition_time
end
local func = running_cutscene.func
if not func then
return
end
local current_waypoint = running_cutscene.waypoints[waypoint_index + 2]
if not current_waypoint or current_waypoint.terminate then
Public.terminate_cutscene(player_index, ticks)
return
end
local params = {
position = current_waypoint.position,
time_to_wait = current_waypoint.time_to_wait,
transition_time = current_waypoint.transition_time,
zoom = current_waypoint.zoom,
name = current_waypoint.name,
tick = tick
}
debug_print('Waypoint_index ' .. waypoint_index + 1 .. ' (waypoint #' .. waypoint_index + 2 .. ') callback in ' .. ticks .. ' ticks')
set_timeout_in_ticks(ticks, callback_function, {func = running_cutscene.func, player_index = player_index, waypoint_index = waypoint_index, params = params})
end
function Public.goTo(player_index, waypoint_index)
local running_cutscene = running_cutscenes[player_index]
if waypoint_index < 0 or waypoint_index > #running_cutscene.waypoints - 2 then
return false
end
Token.get(remove_renderings)(running_cutscene.rendering)
game.get_player(player_index).jump_to_cutscene_waypoint(waypoint_index)
handler({player_index = player_index, waypoint_index = waypoint_index - 1, tick = game.tick})
running_cutscene.current_index = waypoint_index
return true
end
local function restore(event)
Public.terminate_cutscene(event.player_index)
end
Event.add(defines.events.on_cutscene_waypoint_reached, handler)
Event.add(defines.events.on_pre_player_left_game, restore)
Event.add(defines.events.on_player_joined_game, restore)
local replay_cutscene =
Token.register(
function(params)
Public.register_running_cutscene(params.event.player_index, replay.identifier, replay.final_transition_time)
end
)
local function replay_handler(_, player)
if not replay.identifier then
player.print({'cutscene_controller.cannot_replay'})
return
end
Token.get(replay_cutscene)({event = {player_index = player.index}})
end
Command.add(
'replay',
{
description = {'cutscene_controller.replay'},
capture_excess_arguments = false,
allowed_by_server = false
},
replay_handler
)
local function skip_cutscene(_, player)
if not player or not player.valid then
return
end
if player.controller_type == defines.controllers.cutscene then
Public.terminate_cutscene(player.index)
end
end
Command.add(
'skip',
{
description = {'cutscene_controller.skip'},
capture_excess_arguments = false,
allowed_by_server = false
},
skip_cutscene
)
Gui.on_click(
skip_btn_name,
function(event)
skip_cutscene(nil, game.get_player(event.player_index))
end
)
Gui.on_click(
backward_btn_name,
function(event)
local player_index = event.player_index
if Public.goTo(player_index, running_cutscenes[player_index].current_index - 1) == false then
game.get_player(player_index).print("Cutscene: You're already at the beginning")
end
end
)
Gui.on_click(
forward_btn_name,
function(event)
local player_index = event.player_index
if Public.goTo(event.player_index, running_cutscenes[player_index].current_index + 1) == false then
game.get_player(player_index).print("Cutscene: You're already at the end")
end
end
)
return Public

View File

@ -0,0 +1,341 @@
local RS = require 'map_gen.shared.redmew_surface'
local Debug = require 'utils.debug'
local Rendering = require 'utils.rendering'
local Vertices = require 'resources.vertices'
local insert = table.insert
local toggle_debug = false --Set to true if you wish to get spammed with debug messages from the rendering module (Requires _DEBUG = true)
local function debug_print(message, trace_levels)
if toggle_debug then
Debug.print(message, trace_levels)
end
end
local Public = {}
--At zoom level 1 a tile is 32x32 pixels
--tile size is calculated by 32 * zoom level.
local function text_height_in_tiles(scale, zoom)
-- Default (count-font) at scale 10 is 125 pixels for lower letters and 109 for capital letters
-- At scale 5 this is 64 or very close to half of the size at scale 10 (62.5)
-- Therefore size hsa been determined to be (12.5 * scale) + 1
local size = (12.5 * scale) + 1
local pixel_per_tile = zoom * 32
return size / pixel_per_tile, size
end
local function calculate_percentages(settings, player_resolution)
local original_resolution = settings.original_resolution
return {
height = player_resolution.height / original_resolution.height,
width = player_resolution.width / original_resolution.width,
tile = (settings.original_zoom * 32) / (settings.player_zoom * 32)
}
end
local function text_scale(percentage, scale)
return scale * (percentage.height + percentage.width) * 0.5
end
local function fit_to_screen(percentage, coordinates)
if not coordinates.fitted then
local height = percentage.height
local width = percentage.width
local tile = percentage.tile
for _, pos in pairs(coordinates) do
if type(pos) == 'number' then
coordinates.x = coordinates.x * width
coordinates.y = coordinates.y * height
break
else
pos.x = pos.x * width
pos.y = pos.y * height
end
end
for _, pos in pairs(coordinates) do
if type(pos) == 'number' then
coordinates.x = coordinates.x * tile
coordinates.y = coordinates.y * tile
break
else
pos.x = pos.x * tile
pos.y = pos.y * tile
end
end
coordinates.fitted = true
end
return coordinates
end
local function fit_to_screen_edges(settings, player_resolution, coordinates)
if not coordinates.fitted then
local tile = settings.original_zoom * 32
local player_tile = settings.player_zoom * 32
local display_scale = player_resolution.scale or 1
local player_height = (player_resolution.height / player_tile) * 0.5
local player_width = (player_resolution.width / player_tile) * 0.5
for _, pos in pairs(coordinates) do
if type(pos) == 'number' then
coordinates.y = -player_height + (((coordinates.y * tile) / player_tile) * display_scale)
coordinates.x = -player_width + (((coordinates.x * tile) / player_tile) * display_scale)
break
else
pos.y = -player_height + (((pos.y * tile) / player_tile) * display_scale)
pos.x = -player_width + (((pos.x * tile) / player_tile) * display_scale)
end
end
coordinates.fitted = true
end
return coordinates
end
local function create_background_params(params)
local background_params = params.background
if background_params then
for k, v in pairs(params) do
if k ~= 'background' then
if not background_params[k] then
background_params[k] = v
end
end
end
else
background_params = params
end
return background_params
end
local function text_background(settings, offset, player, percentages, size, number_text, params)
local margin = 0.01 / params.scale
local left_top = fit_to_screen(percentages, {x = -40, y = 0})
local right_bottom = fit_to_screen(percentages, {x = 40, y = 0})
left_top.y = -size * margin * 0.875
right_bottom.y = size * (1.5 + (margin * 1.125)) * number_text
local background_params = create_background_params(params)
return Public.draw_rectangle(settings, offset, left_top, right_bottom, player, background_params)
end
function Public.draw_text(settings, offset, text, player, params, draw_background, fit_to_edge)
local ids = {}
local player_resolution = player.display_resolution
player_resolution.scale = player.display_scale
local percentages = calculate_percentages(settings, player_resolution)
local scale = params.scale
if draw_background ~= -1 then
scale = text_scale(percentages, scale)
local size = text_height_in_tiles(scale, settings.player_zoom)
if fit_to_edge then
offset = fit_to_screen_edges(settings, player_resolution, offset)
else
offset = fit_to_screen(percentages, offset)
end
offset.y = offset.y - size * 0.5
end
local size = text_height_in_tiles(scale, settings.player_zoom)
if draw_background == true then
insert(ids, text_background(settings, offset, player, percentages, size, 1, params, fit_to_edge))
end
local target = {x = player.position.x + offset.x, y = player.position.y + offset.y}
local color = params.color
color = color and color or {r = 255, g = 255, b = 255}
local font = params.font
local surface = params.surface
surface = surface or RS.get_surface()
local ttl = params.time_to_live
ttl = ttl and ttl or -1
local forces = params.forces
local players = params.players
players = players or {}
table.insert(players, player)
local visible = params.visible
visible = visible or true
local dog = params.draw_on_ground
dog = dog or false
local orientation = params.orientation
orientation = orientation or 0
local alignment = params.alignment
alignment = alignment or 'center'
--local swz = params.scale_with_zoom
local swz = true
local oiam = params.only_in_alt_mode
oiam = oiam or false
local rendering_params = {
text = {'', text},
color = color,
target = target,
scale_with_zoom = swz,
surface = surface,
time_to_live = ttl,
alignment = alignment,
players = players,
scale = scale,
forces = forces,
visible = visible,
draw_on_ground = dog,
only_in_alt_mode = oiam,
orientation = orientation,
font = font
}
debug_print(rendering_params)
insert(ids, rendering.draw_text(rendering_params))
return ids
end
function Public.draw_multi_line_text(settings, offset, texts, player, params, draw_background, fit_to_edge)
local ids = {}
local player_resolution = player.display_resolution
player_resolution.scale = player.display_scale
local percentages = calculate_percentages(settings, player_resolution)
local scale = params.scale
scale = text_scale(percentages, scale)
local size = text_height_in_tiles(scale, settings.player_zoom)
if fit_to_edge then
offset = fit_to_screen_edges(settings, player_resolution, offset)
else
offset = fit_to_screen(percentages, offset)
end
offset.y = offset.y - size * 0.5
if draw_background then
insert(ids, text_background(settings, offset, player, percentages, size, #texts, params, fit_to_edge))
draw_background = -1
end
for i = 1, #texts do
insert(ids, Public.draw_text(settings, offset, texts[i], player, params, draw_background, fit_to_edge)[1])
offset.y = offset.y + (size * 1.5)
end
return ids
end
function Public.draw_rectangle(settings, offset, left_top, right_bottom, player, params, fit_to_edge)
local player_resolution = player.display_resolution
player_resolution.scale = player.display_scale
local percentages = calculate_percentages(settings, player_resolution)
if fit_to_edge then
offset = fit_to_screen_edges(settings, player_resolution, offset)
left_top = fit_to_screen_edges(settings, player_resolution, left_top)
right_bottom = fit_to_screen_edges(settings, player_resolution, right_bottom)
else
offset = fit_to_screen(percentages, offset)
left_top = fit_to_screen(percentages, left_top)
right_bottom = fit_to_screen(percentages, right_bottom)
end
local target_left = {x = player.position.x + left_top.x + offset.x, y = player.position.y + left_top.y + offset.y}
local target_right = {x = player.position.x + right_bottom.x + offset.x, y = player.position.y + right_bottom.y + offset.y}
local color = params.color
color = color and color or {}
local width = params.width
width = width and width or 0
local filled = params.filled
filled = filled and filled or true
local surface = params.surface
surface = surface or RS.get_surface()
local ttl = params.time_to_live
ttl = ttl and ttl or -1
local forces = params.forces
local players = params.players
players = players or {}
table.insert(players, player)
local visible = params.visible
visible = visible or true
local dog = params.draw_on_ground
dog = dog or false
local oiam = params.only_in_alt_mode
oiam = oiam or false
local rendering_params = {
color = color,
width = width,
filled = filled,
left_top = target_left,
right_bottom = target_right,
surface = surface,
time_to_live = ttl,
forces = forces,
players = players,
visible = visible,
draw_on_ground = dog,
only_in_alt_mode = oiam
}
debug_print(rendering_params)
return rendering.draw_rectangle(rendering_params)
end
local blackout_settings = {original_resolution = {height = 1440, width = 2560}, original_zoom = 1, player_zoom = 1}
function Public.blackout(player, zoom, ttl, color)
local left_top = {x = -40, y = -22.5}
local right_bottom = {x = 40, y = 22.5}
blackout_settings.player_zoom = zoom
return Public.draw_rectangle(blackout_settings, {x = 0, y = 0}, left_top, right_bottom, player, {color = color, time_to_live = ttl})
end
function Public.draw_arrow(settings, offset, player, params, fit_to_edge)
local player_resolution = player.display_resolution
player_resolution.scale = player.display_scale
local percentages = calculate_percentages(settings, player_resolution)
if fit_to_edge then
offset = fit_to_screen_edges(settings, player_resolution, offset)
else
offset = fit_to_screen(percentages, offset)
end
local vertices = Rendering.scale(Vertices.arrow, percentages.tile, percentages.tile)
vertices = Rendering.rotate(vertices, params.rotation)
vertices = Rendering.translate(vertices, offset.x, offset.y)
local color = params.color or {1, 1, 1, 1}
params.color = color
local players = params.players
players = players or {}
table.insert(players, player)
params.players = players
params.surface = RS.get_surface()
--Debug.print(vertices)
return Rendering.draw_polygon(vertices, params)
end
return Public

213
features/gui/radio.lua Normal file
View File

@ -0,0 +1,213 @@
local Gui = require 'utils.gui'
local Command = require 'utils.command'
local Event = require 'utils.event'
local main_button_name = Gui.uid_name()
local radio_frame = Gui.uid_name()
local close_radio = Gui.uid_name()
local sounds = {
['ambient'] = {
'after-the-crash',
'automation',
'resource-deficiency',
'are-we-alone',
'beyond-factory-outskirts',
'censeqs-discrepancy',
'efficiency-program',
'expansion',
'the-search-for-iron',
'gathering-horizon',
'research-and-minerals',
'solar-intervention',
'the-oil-industry',
'the-right-tools',
'pollution',
'turbine-dynamics',
'sentient',
'anomaly',
'first-light',
'transmit',
'swell-pad',
'world-ambience-1',
'world-ambience-2',
'world-ambience-3',
'world-ambience-4',
'world-ambience-5',
'world-ambience-6'
},
['default'] = {
'worm-sends-biters',
'mainframe-activated',
'car-repaired'
},
['utility'] = {
'achievement_unlocked',
'alert_destroyed',
'armor_insert',
'armor_remove',
'axe_fighting',
'axe_mining_ore',
'build_big',
'build_medium',
'build_small',
'cannot_build',
'console_message',
'crafting_finished',
'deconstruct_big',
'deconstruct_medium',
'deconstruct_small',
'default_manual_repair',
'game_lost',
'game_won',
'gui_click',
'inventory_move',
'list_box_click',
'metal_walking_sound',
'mining_wood',
'new_objective',
'research_completed',
'scenario_message',
'tutorial_notice',
'wire_connect_pole',
'wire_disconnect',
'wire_pickup'
}
}
local function draw_radio(event)
local frame_caption
frame_caption = 'Radio'
local player = event.player
local center = player.gui.center
local frame = center[radio_frame]
if frame then
Gui.remove_data_recursively(frame)
frame.destroy()
return
end
frame = center.add {type = 'frame', name = radio_frame, caption = frame_caption, direction = 'vertical'}
local scroll_pane =
frame.add {
type = 'scroll-pane',
vertical_scroll_policy = 'auto-and-reserve-space',
horizontal_scroll_policy = 'never'
}
Gui.set_data(scroll_pane, frame)
local main_table = scroll_pane.add {type = 'table', column_count = 4}
for type, sound in pairs(sounds) do
for i = 1, #sound do
local name = (type == 'default') and sound[i] or type .. '/' .. sound[i]
local textbox = main_table.add {type = 'text-box', text = type .. '/' .. sound[i]}
textbox.read_only = true
textbox.style.height = 28
textbox.style.width = 250
local button = main_table.add {type = 'button', name = 'radio_play:' .. name, caption = 'Play'}
button.style.width = 54
end
end
local information_pane =
frame.add {
type = 'scroll-pane',
vertical_scroll_policy = 'auto-and-reserve-space',
horizontal_scroll_policy = 'never'
}
information_pane.style.horizontally_stretchable = true
information_pane.style.horizontal_align = 'center'
Gui.set_data(information_pane, frame)
local text =
[[
Other types:
syntax = <type>/<entity>
"tile-walking" - for example "tile-walking/concrete"
"tile-build"
"tile-mined"
"entity-build" - for example "entity-build/wooden-chest"
"entity-mined"
"entity-vehicle_impact"
"entity-open"
"entity-close"
]]
local information = information_pane.add {type = 'text-box', text = text}
information.style.horizontally_stretchable = true
information.style.vertically_stretchable = true
information.style.minimal_height = 200
information.style.minimal_width = 400
local bottom_flow = frame.add {type = 'flow', direction = 'horizontal'}
local left_flow = bottom_flow.add {type = 'flow', direction = 'horizontal'}
left_flow.style.horizontal_align = 'left'
left_flow.style.horizontally_stretchable = true
local close_button = left_flow.add {type = 'button', name = close_radio, caption = 'Close'}
Gui.set_data(close_button, frame)
player.opened = frame
end
Gui.on_click(main_button_name, draw_radio)
Gui.on_click(
close_radio,
function(event)
local frame = Gui.get_data(event.element)
Gui.remove_data_recursively(frame)
frame.destroy()
end
)
Gui.on_custom_close(
radio_frame,
function(event)
local element = event.element
Gui.remove_data_recursively(element)
element.destroy()
end
)
local function radio_command(_, player)
if player and player.valid then
local event = {player = player}
draw_radio(event)
end
end
Command.add(
'radio',
{
description = 'Opens radio gui',
capture_excess_arguments = false,
allowed_by_server = false
},
radio_command
)
local function handler(event)
local element = event.element
if not element or not element.valid then
return
end
local name = element.name
local subname = string.sub(name, 1, 11)
if subname == 'radio_play:' then
local path = string.sub(name, 12)
local player = game.get_player(event.player_index)
if (game.is_valid_sound_path(path)) then
player.play_sound {path = path}
return
else
player.print('Unable to play sound: ' .. path)
end
end
end
Event.add(defines.events.on_gui_click, handler)

View File

@ -6,25 +6,10 @@ local pairs = pairs
local get_random_weighted = table.get_random_weighted
local function player_created(event)
local Public = {}
function Public.show_start_up(player)
local config = global.config.player_create
local player = game.get_player(event.player_index)
if not player or not player.valid then
return
end
-- ensure the top menu is correctly styled
local gui = player.gui
gui.top.style = 'slot_table_spacing_horizontal_flow'
gui.left.style = 'slot_table_spacing_vertical_flow'
local player_insert = player.insert
for _, item in pairs(config.starting_items) do
player_insert(item)
end
local p = player.print
for _, message in pairs(config.join_messages) do
p(message)
@ -48,12 +33,29 @@ local function player_created(event)
elseif not _DEBUG and not game.is_multiplayer() then
player.print('To change your name in single-player, open chat and type the following /c game.player.name = "your_name"')
end
end
-- Remove 2019-03-04
if player.admin then
player.print('## - Regular commands have changed. /regular <player_name> to promote /regular-remove <player_name> to demote.')
local function player_created(event)
local config = global.config.player_create
local player = game.get_player(event.player_index)
if not player or not player.valid then
return
end
-- ensure the top menu is correctly styled
local gui = player.gui
gui.top.style = 'slot_table_spacing_horizontal_flow'
gui.left.style = 'slot_table_spacing_vertical_flow'
local player_insert = player.insert
for _, item in pairs(config.starting_items) do
player_insert(item)
end
if not config.cutscene then
Public.show_start_up(player)
end
--End remove
end
Event.add(defines.events.on_player_created, player_created)
@ -125,3 +127,5 @@ if _CHEATS then
Event.add(defines.events.on_player_created, player_created_cheat_mode)
end
return Public

View File

@ -20,7 +20,7 @@ show_rail_block=Toggles rail block visualisation.
server_time=Prints the server's time.
seeds=List the seeds of all surfaces
redmew_version=Prints the version of the Redmew scenario
whois=provides information about a given player, admins can see the inventory of a player by adding "yes" as a second argument
whois=Provides information about a given player, admins can see the inventory of a player by adding "yes" as a second argument
meltdown_get=Gets the status of meltdown.
meltdown_set=Sets the status of meltdown.
redmew_color=Set will save your current color for future maps. Reset will erase your saved color. Random will give you a random color.

View File

@ -147,6 +147,12 @@ player_distance_walked=Distance walked
satellites_launched=Satellites launched
unknown_death_cause=Unknown
[cutscene_controller]
cannot_start_new=Can not start cutscene, you need to finish your current one. Try to run /skip before trying again
skip=Skips the current cutscene
replay=Replays the introduction cutscene
cannot_replay=There is no cutscene to replay
[autofill]
insert_item=-__1__ __2__ (__3__)
enable=Enable autofill

View File

@ -43,6 +43,28 @@ score_cave_collapses=Cave collapses
score_mine_size=Mine size
score_experience_lost=Experience lost
cutscene_case_line2=Welcome to __1__
cutscene_case_line4=This is a custom scenario developed by __1__
cutscene_case_line5=Join us at __1__
cutscene_case_line6=The following introduction will help you get started!
cutscene_case_line7=If you experience issues duing this cutscene let us know
cutscene_case0_line1=This is the starting area
cutscene_case0_line3=Expanding the mine is dangerous!
cutscene_case1_line1=Walls are used to keep the cave roof from crushing us
cutscene_case2_line1=The market provides extra supplies in exchange for coins
cutscene_case2_line3=You unlock new items when you level up
cutscene_case3_line1=Cave ins happens frequently when you don't add supports
cutscene_case3_line3=Different types of brick and concrete can reinforce our support pillars!
cutscene_case4_line1=This world contains brittle rocks
cutscene_case4_line3=Our tools are too powerful to preserve any resources from destroying them
cutscene_case5_line1=Most actions give experience!
cutscene_case5_line3=The floating text indicates the quantity and cause of the experience
cutscene_case6_line1=The native population is lurking in the dark
cutscene_case6_line3=Be wary when digging, always bring along some defences
cutscene_case7_line1=This concludes the introduction
cutscene_case7_line3=Have fun and keep digging!
replay_cutscene=To replay the introduction, use the __1__ command
# locale linked to the quadrants scenario
[quadrants]
on=ON

View File

@ -49,6 +49,10 @@ local Config = {
starting_items = {}
}
},
-- controls the introduction cutscene
cutscene = {
enabled = true
},
-- core feature
diggy_hole = {
enabled = true,

View File

@ -0,0 +1,305 @@
local Event = require 'utils.event'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Debug = require 'utils.debug'
local Cutscene = require 'features.cutscene.cutscene_controller'
local CS_Rendering = require 'features.cutscene.rendering'
local RS = require 'map_gen.shared.redmew_surface'
local Color = require 'resources.color_presets'
local PC = require 'features.player_create'
local Experience = require 'map_gen.maps.diggy.feature.experience'
local register_rendering = Cutscene.register_rendering_id
local play_sound = Cutscene.play_sound
local draw_text = CS_Rendering.draw_text
local draw_multi_line = CS_Rendering.draw_multi_line_text
local rad = math.rad
local Rendering = require 'utils.rendering'
local DiggyCutscene = {}
local delayed_draw_text =
Token.register(
function(params)
local tick = params.tick
local player = params.player
if params.play_sound > 1 then
play_sound(tick, player, 'utility/list_box_click', 1)
end
register_rendering(player.index, tick, draw_text(params.settings, params.offset, params.text, params.player, params.params, params.draw_background, params.fit_to_edge))
end
)
local function draw_text_auto_replacing(tick, settings, offset, texts, player, params, draw_background, fit_to_edge, time, between_time)
time = time or 400
time = time / #texts
between_time = between_time or 30
params.time_to_live = time - between_time
if params.background then
params.background.time_to_live = time - between_time
end
for i = 1, #texts do
if texts[i] ~= '' then
Task.set_timeout_in_ticks(time * (i - 1), delayed_draw_text, {tick = tick, settings = settings, offset = offset, text = texts[i], player = player, params = params, draw_background = draw_background, fit_to_edge = fit_to_edge, play_sound = i})
end
end
end
local delayed_draw_arrow =
Token.register(
function(params)
local player = params.player
local tick = params.tick
params = params.params
local rendering_parmas = params.params
local id = CS_Rendering.draw_arrow(params.settings, params.offset, player, rendering_parmas, params.fit_to_edge)
register_rendering(player.index, tick, id)
Rendering.blink(id, 20, rendering_parmas.time_to_live)
end
)
local function delayed_function(func, player, tick, params, offset_time)
Task.set_timeout_in_ticks(offset_time, func, {player = player, tick = tick, params = params})
end
local delayed_fade_blackout =
Token.register(
function(params)
local render_params = params.params
local id = CS_Rendering.blackout(params.player, render_params.zoom, render_params.time_to_live, render_params.color)
register_rendering(params.player.index, params.tick, id)
Rendering.fade(id, render_params.time_to_live - 1, 10)
end
)
local original_resolution = {height = 1440, width = 2560}
local original_zoom = 1
local function cutscene_function(player_index, waypoint_index, params)
local cases = {}
local player = game.players[player_index]
local ttw = params.time_to_wait
local zoom = params.zoom
local tick = params.tick
local settings = {original_resolution = original_resolution, original_zoom = original_zoom, player_zoom = zoom}
if waypoint_index ~= -1 then
play_sound(tick, player, 'utility/list_box_click', 1)
--play_sound(tick, player, 'utility/inventory_move', 1, 10)
end
cases[-1] = function()
play_sound(tick, player, 'utility/game_won')
play_sound(tick, player, 'ambient/first-light', 1, 550)
register_rendering(player_index, tick, CS_Rendering.blackout(player, zoom, ttw + 1))
register_rendering(player_index, tick, draw_text(settings, {x = 0, y = -16}, 'Diggy', player, {scale = 10, time_to_live = ttw, color = Color.yellow}, false, false))
register_rendering(
player_index,
tick,
draw_multi_line(settings, {x = 0, y = -5}, {{'diggy.cutscene_case_line2', 'Diggy'}, '---------------------', {'diggy.cutscene_case_line4', 'Redmew'}, {'diggy.cutscene_case_line5', 'www.redmew.com/discord'}}, player, {scale = 5, time_to_live = ttw}, false)
)
draw_text_auto_replacing(tick, settings, {x = 0, y = 10}, {'', {'diggy.cutscene_case_line6'}}, player, {scale = 3}, false, false, ttw, 0)
draw_text_auto_replacing(tick, settings, {x = 0, y = 16}, {'', '', {'diggy.cutscene_case_line7'}}, player, {scale = 1}, false, false, ttw, 0)
end
cases[0] = function()
register_rendering(player_index, tick, CS_Rendering.blackout(player, zoom, ttw + 1))
register_rendering(player_index, tick, draw_text(settings, {x = 0, y = 0}, 'Redmew - Diggy', player, {scale = 10, time_to_live = ttw - 60, color = Color.red}, false, false))
register_rendering(player_index, tick, draw_text(settings, {x = 0, y = -5}, 'Introduction', player, {scale = 5, time_to_live = ttw - 60}, false, false))
delayed_function(delayed_draw_arrow, player, tick, {settings = settings, offset = {x = 7, y = 2.5}, params = {rotation = rad(-45), time_to_live = 275 * 3 - 30}, fit_to_edge = true}, 0)
draw_text_auto_replacing(tick, settings, {x = 8.5, y = 3}, {'This is our toolbar!'}, player, {scale = 2.5, alignment = 'left'}, false, true, 275)
draw_text_auto_replacing(tick, settings, {x = 8.5, y = 3}, {'', "Here you'll find a wide range of tools and informations about us!"}, player, {scale = 2.5, alignment = 'left'}, false, true, 275 * 2)
draw_text_auto_replacing(tick, settings, {x = 8.5, y = 3}, {'', '', 'Hover your mouse over them for more information'}, player, {scale = 2.5, alignment = 'left'}, false, true, 275 * 3)
delayed_function(delayed_draw_arrow, player, tick, {settings = settings, offset = {x = 1, y = 2.5}, params = {rotation = rad(-45), time_to_live = 275 - 30}, fit_to_edge = true}, 275 * 3)
draw_text_auto_replacing(tick, settings, {x = 2.5, y = 3}, {'', '', '', 'You can toggle our toolbar with this button'}, player, {scale = 2.5, alignment = 'left'}, false, true, 275 * 4)
delayed_function(delayed_draw_arrow, player, tick, {settings = settings, offset = {x = 3.5, y = 2.5}, params = {rotation = rad(-45), time_to_live = 275 - 30}, fit_to_edge = true}, 275 * 4.5)
draw_text_auto_replacing(tick, settings, {x = 5, y = 3}, {'', '', '', '', 'This is the Diggy experience menu'}, player, {scale = 2.5, alignment = 'left'}, false, true, 275 * 5.5)
delayed_function(delayed_draw_arrow, player, tick, {settings = settings, offset = {x = 15, y = 9}, params = {rotation = rad(-90), time_to_live = 275 - 30}, fit_to_edge = true}, 275 * 5.5)
Cutscene.toggle_gui(tick, player, Experience, 275 * 5.5, 'left')
draw_text_auto_replacing(tick, settings, {x = 17, y = 8.7}, {'', '', '', '', '', 'Here you can see the current progress of the mine'}, player, {scale = 2.5, alignment = 'left'}, false, true, 275 * 6.5)
Cutscene.toggle_gui(tick, player, Experience, 275 * 6.5)
delayed_function(delayed_fade_blackout, player, tick, {zoom = zoom, time_to_live = 120 + 61, color = {0, 0, 0, 1}}, ttw - 61)
end
cases[1] = function()
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case0_line1'}, {'diggy.cutscene_case0_line3'}}, player, {scale = 2.5}, true, false, ttw)
local entity = RS.get_surface().find_entities_filtered {position = {0, 0}, radius = 20, name = 'stone-wall', limit = 1}
if entity[1] then
local position = entity[1].position
local waypoint = {
-- case 2
position = position,
transition_time = 120,
time_to_wait = 275,
zoom = 5
}
Debug.print_position(position, 'position of wall')
Cutscene.inject_waypoint(player_index, waypoint, waypoint_index + 3, true)
end
end
cases[2] = function()
--play_sound(tick, player, 'utility/build_small', 1, 25)
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case1_line1'}}, player, {scale = 2.5}, true, false, ttw)
end
cases[3] = function()
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case2_line1'}, {'diggy.cutscene_case2_line3'}}, player, {scale = 2.5}, true, false, ttw)
end
cases[4] = function()
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case3_line1'}, {'diggy.cutscene_case3_line3'}}, player, {scale = 2.5}, true, false, ttw)
local radius = 10
local entity
repeat
entity = RS.get_surface().find_entities_filtered {position = {0, 0}, radius = radius, name = 'rock-big', limit = 1}
if radius <= 10 then
radius = 0
end
radius = radius + 25
until entity[1] or radius >= 200
local position = {0, 3.5}
local way_zoom = 0.4
entity = entity[1]
if entity then
position = entity.position
way_zoom = 5
Debug.print_position(position, 'position of rock')
end
local waypoint = {
-- case 5
position = position,
transition_time = 120,
time_to_wait = 550,
zoom = way_zoom
}
Cutscene.inject_waypoint(player_index, waypoint, waypoint_index + 3)
end
cases[5] = function()
play_sound(waypoint_index, player, 'utility/axe_mining_ore', 3, 35)
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case4_line1'}, {'diggy.cutscene_case4_line3'}}, player, {scale = 2.5}, true, false, ttw)
end
cases[6] = function()
play_sound(tick, player, 'utility/research_completed', 1, 5)
local exp = 2500
local text = {'', '[img=item/automation-science-pack] ', {'diggy.float_xp_gained_research', exp}}
player.create_local_flying_text {position = params.position, text = text, color = Color.light_sky_blue, time_to_live = ttw / 3}
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case5_line1'}, {'diggy.cutscene_case5_line3'}}, player, {scale = 2.5}, true, false, ttw)
end
cases[7] = function()
play_sound(tick, player, 'utility/axe_fighting', 5, 25, 10)
play_sound(tick, player, 'worm-sends-biters', 1, 70)
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case6_line1'}, {'diggy.cutscene_case6_line3'}}, player, {scale = 2.5}, true, false, ttw)
end
cases[8] = function()
draw_text_auto_replacing(tick, settings, {x = 0, y = 18}, {{'diggy.cutscene_case7_line1'}, {'diggy.cutscene_case7_line3'}}, player, {scale = 2.5}, true, false, ttw)
--play_sound(tick, player, 'utility/tutorial_notice', 1)
end
local case = cases[waypoint_index]
if case then
case()
end
end
local waypoints = {
{
-- case -1
position = {x = 0, y = 0},
transition_time = 60,
time_to_wait = 600,
zoom = 0.5
},
{
-- case -1.1
position = {x = 0, y = 0},
transition_time = 0,
time_to_wait = 275 * 7,
zoom = 0.5
},
{
-- case 0
position = {x = 0, y = 0},
transition_time = 120,
time_to_wait = 550,
zoom = 1
},
{
-- case 1
position = {x = 0, y = 0},
transition_time = 120,
time_to_wait = 275,
zoom = 1.5
},
{
-- case 2
position = {x = 0.5, y = 3.5},
transition_time = 120,
time_to_wait = 550,
zoom = 5
},
{
-- case 3
position = {x = 0, y = 0},
transition_time = 120,
time_to_wait = 550,
zoom = 2
},
-- inserting case 4
{
-- case 5
position = {x = 0, y = -2},
transition_time = 120,
time_to_wait = 550,
zoom = 1.8
},
{
-- case 6
position = {x = 0, y = 0},
transition_time = 120,
time_to_wait = 550,
zoom = 0.3
},
{
-- case 7
position = {x = 0, y = 0},
transition_time = 120,
time_to_wait = 430,
zoom = 0.8
}
}
local function terminate_function(player_index)
local player = game.get_player(player_index)
PC.show_start_up(player)
player.print({'diggy.replay_cutscene', '/replay'}, Color.yellow)
end
Cutscene.register_cutscene_function('Diggy_Welcome', waypoints, Token.register(cutscene_function), Token.register(terminate_function))
Cutscene.register_replay('Diggy_Welcome', 120)
local start_cutscene =
Token.register(
function(params)
Cutscene.register_running_cutscene(params.event.player_index, 'Diggy_Welcome', 120)
end
)
function DiggyCutscene.register()
global.config.player_create.cutscene = true
Event.add(
defines.events.on_player_created,
function(event)
Task.set_timeout_in_ticks(60, start_cutscene, {event = event})
end
)
end
return DiggyCutscene

View File

@ -489,7 +489,7 @@ local function redraw_buff(data)
end
end
local function toggle(event)
function Experience.toggle(event)
local player = event.player
local gui = player.gui
local left = gui.left
@ -559,7 +559,7 @@ end
Gui.allow_player_to_toggle_top_element_visibility('Diggy.Experience.Button')
Gui.on_click('Diggy.Experience.Button', toggle)
Gui.on_click('Diggy.Experience.Button', Experience.toggle)
Gui.on_custom_close(
'Diggy.Experience.Frame',
function(event)
@ -576,7 +576,7 @@ local function update_gui()
if frame and frame.valid then
local data = {player = p, trigger = 'update_gui'}
toggle(data)
Experience.toggle(data)
end
end

16
resources/vertices.lua Normal file
View File

@ -0,0 +1,16 @@
local shapes = {
arrow_point = {
--triangle
{1, 1}, -- right edge g
{-1, 1}, -- left edge b
{0, 0}, -- top (pointer) a
--rectangle
{0.5, 1}, -- right inner top f
{0.5, 2}, -- right inner bottom e
{-0.5, 1}, -- left inner top c
{-0.5, 2} -- left inner bottom d
},
arrow = {{1, 0}, {-1, 0}, {0, -1}, {0.5, 0}, {0.5, 1}, {-0.5, 0}, {-0.5, 1}}
}
return shapes

114
utils/rendering.lua Normal file
View File

@ -0,0 +1,114 @@
local Token = require 'utils.token'
local Task = require 'utils.task'
local Public = {}
local cos = math.cos
local sin = math.sin
local rendering = rendering
local draw_polygon = rendering.draw_polygon
function Public.draw_polygon(positions, options)
local vertices = {}
for i = 1, #positions do
vertices[i] = {target = positions[i]}
end
local args = {vertices = vertices}
for k, v in pairs(options) do
args[k] = v
end
return draw_polygon(args)
end
function Public.translate(positions, x, y)
local result = {}
for i = 1, #positions do
local pos = positions[i]
result[i] = {pos[1] + x, pos[2] + y}
end
return result
end
function Public.scale(positions, x, y)
local result = {}
for i = 1, #positions do
local pos = positions[i]
result[i] = {pos[1] * x, pos[2] * y}
end
return result
end
function Public.rotate(positions, radians)
local qx = cos(radians)
local qy = sin(radians)
local result = {}
for i = 1, #positions do
local pos = positions[i]
local x, y = pos[1], pos[2]
local rot_x = qx * x - qy * y
local rot_y = qy * x + qx * y
result[i] = {rot_x, rot_y}
end
return result
end
local fade_token =
Token.register(
function(params)
local id = params.id
if rendering.is_valid(id) then
rendering.set_color(id, params.color)
end
end
)
function Public.fade(id, time, ticks)
ticks = ticks or 20
local count = (time - time % ticks) / ticks
if rendering.is_valid(id) then
local color = rendering.get_color(id)
local a = color.a or 1
local decrement = a / count
for i = 1, count do
a = a - decrement
a = a >= 0 and a or 0
Task.set_timeout_in_ticks(ticks * i, fade_token, {id = id, color = {r = color.r, b = color.b, g = color.g, a = a}})
end
end
end
local blink_token =
Token.register(
function(params)
local id = params.id
if rendering.is_valid(id) then
rendering.set_visible(id, params.visible)
end
end
)
function Public.blink(id, rate, time)
local count = (time - time % rate) / rate
rate = (time / count) * 2
if rendering.is_valid(id) then
local visible = rendering.get_visible(id)
for i = 1, count do
visible = not visible
Task.set_timeout_in_ticks(rate * i, blink_token, {id = id, visible = visible})
end
end
end
return Public