Last active
September 30, 2015 12:28
-
-
Save kl/1790186 to your computer and use it in GitHub Desktop.
Event Spawn for VX Ace 1.2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# EventSpawn 1.2 | |
# by Kal (github.com/kl) | |
# | |
# --- Changelog ---- | |
# | |
# 1.2 - Can now execute common event on spawn. | |
# 1.1.2 - Fix event_name_not_found_msg typo. | |
# 1.1.1 - Fix absolute positioning bug. | |
# 1.1 - Can now spawn using an event id as well as an event name. | |
# 1.0.2 - Improved error messages. | |
# 1.0.1 - Fix a bug where <spawn-max> would spawn one less than specified. | |
# 1.0 - Initial release. | |
# | |
# --- How To Use ---- | |
# | |
# First create an event on the map that will act as the spawner event. | |
# Now you can add various spawn tags to this event (using the Comment function) | |
# to let the script know what you want it to do. | |
# | |
# The spawn tag comments can be added to any event page. | |
# | |
# The following commands are available (note do not put in the [ and ]) | |
# | |
# (NOTE: all the below tags are optional, except for spawn-event and spawn-timer) | |
# | |
# <spawn-event [name of event to spawn]> | |
# <spawn-timer [number]> | |
# <spawn-pos [position string]> | |
# <spawn-type [type name]> | |
# <spawn-switch [switch ID]> | |
# <spawn-max [number]> | |
# <spawn-life [number]> | |
# <spawn-on-passable [boolean]> | |
# <spawn-common-event [common event id]> | |
# | |
# <spawn-event> takes the name of the event to spawn (the name can be found | |
# in the upper left corner of the event box) | |
# | |
# <spawn-timer> takes the number of frames between each spawn (60 frames = 1 sec) | |
# | |
# <spawn-pos> takes a string that defines where to spawn the events. It can take | |
# either a relative or an absolute position. To specify a relative position, | |
# use up, down, left and right. Then the event will be spawned relative to | |
# the spawner on tile in that direction. | |
# To use an absolute position you can give the map coordinate like this | |
# x,y for example 13,49. | |
# You can also set the events to spawn a random distance from the spawner | |
# event. To do that use the tag like this: <spawn-pos random:[number]> | |
# The number after the colon is the max distance from the spawner that | |
# events should be able to spawn. If you omit this number the max distance | |
# is set to 1. | |
# If you do not set the spawn-pos, the event will be spawned on the same | |
# tile as the spawner is on. | |
# | |
# <spawn-type> takes the type of spawner to use. Currently this script supports | |
# the normal type (which you get if you don't use this tag) and the | |
# "loop" type which will keep track of the events that have been spawned, | |
# and if any of them dies it will be able to spawn again (if it is using | |
# the spawn-max tag that is) | |
# | |
# <spawn-switch> takes the ID of a switch and the spawner will only spawn if | |
# this switch is on. | |
# | |
# <spawn-max> takes the maximum number of events the spawner should spawn. | |
# Once this number has been reached the spawner will stop spawning. | |
# | |
# <spawn-life> takes the number of frames a spawned event should exist. | |
# After the time is up, the spawned event will be automatically | |
# despawned (erased). If you don't set this options the spawned | |
# events will not be despawned at all. | |
# | |
# <spawn-on-passable> takes true or false and determines whether the spawner | |
# should be able to spawn events on unpassable tiles or not (note if you use | |
# absolute positioning this attribute does nothing). The default is true. | |
# So if you set spawn-pos to 'up' and the tile above the spawner event is | |
# unpassable, the event will spawn on an adjecent passable tile. | |
# If no adjecent tile is passable, it will spawn on the same tile as | |
# the spawner event. | |
# | |
# <spawn-common-event> takes the ID of a common event that should be | |
# executed every time the spawner spawns an event. | |
# When the common event executes, you can access the spawner object from | |
# the Script event command by calling @fiber.spawner. You can then set various | |
# options using the spawner.options hash. An example: (the following is a script | |
# that is put in to the Script event command box in the common event) | |
# | |
# spawner = @fiber.spawner | |
# spawner.options[:timer] += 30 | |
# | |
# This code will get the spawner object and then access the options hash where | |
# it increments the timer with 30. So every time the common event is executed, | |
# the timer will be incremented by 30. You can use the following symbols to | |
# access the options data (they correspond with the <spawn> tags): | |
# | |
# options[:timer] | |
# options[:event_name] | |
# options[:event_id] | |
# options[:switch] | |
# options[:max] | |
# options[:life] | |
# options[:passable] | |
# options[:type] | |
# options[:common_event] | |
# | |
# --- Example --- | |
# | |
# Here is what an example event comment for a spawner could look like: | |
# | |
# <spawn-event EV004> | |
# <spawn-timer 120> | |
# <spawn-switch 1> | |
# <spawn-pos random:4> | |
# <spawn-max 3> | |
# <spawn-life 400> | |
# <spawn-type loop> | |
# <spawn-on-passable false> | |
# <spawn-common-event 10> | |
# | |
# --- License --- | |
# | |
# MIT license | |
# http://opensource.org/licenses/MIT | |
# | |
module Kal | |
module EventSpawn | |
module CONFIG | |
# You can set this constant to a map id and the script will look | |
# for events to spawn on that map. That way you can put all your spawn | |
# events on one map, and they don't need to present on the map they | |
# are spawned on. | |
SPAWN_MAP_ID = nil | |
end | |
# <<< END CONFIGURATION >>> | |
class SpawnError < StandardError; end | |
# | |
# The Factory module is used to parse the spawn tags and create the | |
# spawner events. | |
# | |
module Factory | |
module_function | |
# | |
# Creates and returns a spawner given a Game_Event (which is the event | |
# that the spawner will spawn) and an RPG::Event (which is the RPG::Event | |
# for the spawner event and it holds things such as event comments). | |
# | |
# This method is called for every event on the map inside Game_Event's | |
# constructor. If the event it is called for is not a spawner event | |
# then this method returns nil. | |
# | |
def make_spawner(game_event, rpg_event) | |
tags = get_spawn_tags(rpg_event) | |
return nil if tags.empty? | |
options = make_options_hash(tags.join) | |
check_requirements(options, game_event.id) | |
select_spawner_object(game_event, options) | |
end | |
# ***** HELPER METHODS ***** | |
# | |
# Takes an RPG::Event object and returns an array with all the spawn | |
# tag comments (e.g. <spawn-timer 120>) for the event. | |
# | |
def get_spawn_tags(rpg_event) | |
# Returns an array with all comments on all pages. | |
comments = rpg_event.pages.each_with_object([]) do |page, array| | |
array << page.list.select do |cmd| | |
cmd.code == 108 || cmd.code == 408 | |
end | |
end | |
# Selects the comments that are spawn comments and return them. | |
spawns = comments.flatten.select { |c| c.parameters.first =~ /^<spawn-/ } | |
spawns.map { |c| c.parameters.first } | |
end | |
# | |
# Parses a spawn string (all spawn comments as one string) and returns | |
# an options hash. | |
# | |
def make_options_hash(spawn_string) | |
timer = spawn_string[/<spawn-timer\s+(\d+)>/i, 1] | |
event_name = spawn_string[/<spawn-event\s+([\d\w]+)>/i, 1] | |
event_id = spawn_string[/spawn-id\s+(\d+[Xx]?)>/i, 1] | |
max = spawn_string[/<spawn-max\s+(\d+)>/i, 1] | |
switch = spawn_string[/<spawn-switch\s+(\d+)>/i, 1] | |
life = spawn_string[/<spawn-life\s+(\d+)>/i, 1] | |
not_passable = spawn_string[/<spawn-on-passable\s+(false)/i, 1] | |
type = spawn_string[/<spawn-type\s+(\w+)>/i, 1] | |
pos = spawn_string[/<spawn-pos\s+([\d\w,:]+?)>/i, 1] | |
common_event = spawn_string[/spawn-common-event\s+(\d+)/i, 1] | |
options = {} | |
options[:timer] = timer.to_i if timer | |
options[:event_name] = event_name | |
options[:event_id] = event_id | |
options[:switch] = switch.to_i if switch | |
options[:max] = max.to_i if max | |
options[:life] = life.to_i if life | |
options[:passable] = false if not_passable | |
options[:type] = type | |
options[:common_event] = common_event.to_i if common_event | |
if match = pos && pos.match(/(\d+),(\d+)/) | |
options[:position] = :absolute | |
options[:x] = match[1].to_i | |
options[:y] = match[2].to_i | |
else | |
options[:position] = :relative | |
case pos | |
when "up" | |
options[:y_adjust] = -1 | |
when "down" | |
options[:y_adjust] = 1 | |
when "left" | |
options[:x_adjust] = -1 | |
when "right" | |
options[:x_adjust] = 1 | |
when /random/ | |
distance = pos[/random:(\d+)/, 1] || 1 | |
options[:random_pos] = distance.to_i | |
end | |
end | |
options | |
end | |
# | |
# Checks to see if the user set the <spawn-event>/<spawn-id> | |
# and <spawn-timer> tags. If not raise exceptions. | |
# | |
def check_requirements(options, event_id) | |
event_msg = "not set for EVENT:" + "%03d" % event_id | |
if !options[:event_name] && !options[:event_id] | |
raise ArgumentError.new("<spawn-event> or <spawn-id> #{event_msg}") | |
elsif !options[:timer] | |
raise ArgumentError.new("<spawn-timer> #{event_msg}") | |
end | |
end | |
# | |
# Selects the Spawner type to use. | |
# | |
def select_spawner_object(game_event, options) | |
case options[:type] | |
when "loop" | |
LoopSpawner.new(game_event, options) | |
else # when no type was selected | |
Spawner.new(game_event, options) | |
end | |
end | |
end | |
# | |
# Spawner is the class that keeps track of what event should be spawned, | |
# when it should be spawned and where it should be spawned. | |
# Each Game_Event that is a spawner will have it's own reference to a | |
# Spawner object. | |
# | |
class Spawner | |
include Kal::EventSpawn::CONFIG | |
attr_reader :options | |
# | |
# Sets initial values. game_event is the event that will be spawned. | |
# | |
def initialize(game_event, options_hash) | |
@game_event = game_event | |
@options = {x_adjust: 0, y_adjust: 0, passable: true} # default options | |
@options.merge!(options_hash) | |
@timer = @options[:timer] | |
@spawn_count = 0 | |
@spawn_map_events = load_spawn_map_events | |
@common_event_runner = CommonEventRunner.new | |
end | |
# | |
# Updates a the spawner event's timer (if it should spawn or not). | |
# | |
def update | |
return if @options[:switch] && !$game_switches[@options[:switch]] | |
@timer -= 1 | |
if @timer <= 0 | |
@should_spawn = true | |
@timer = @options[:timer] | |
end | |
end | |
# | |
# Spawns a new event. | |
# | |
def spawn | |
run_common_event | |
set_spawn_pos # set the position the spawned event will have. | |
if name = @options[:event_name] | |
event = event_from_name(name) | |
elsif id = @options[:event_id] | |
event = event_from_id(id) | |
end | |
cloned_event = clone_event(event) | |
cloned_event.life_es_kal = @options[:life] | |
cloned_event.moveto(@x, @y) # move the spawned event to it's position. | |
# Add the spawned event to the Game_Map and the current Spriteset: | |
$game_map.events[cloned_event.id] = cloned_event | |
spriteset = SceneManager.scene.instance_eval { @spriteset } | |
spriteset.instance_eval do | |
@character_sprites << Sprite_Character.new(@viewport1, cloned_event) | |
end | |
@spawn_count += 1 | |
@should_spawn = false | |
end | |
# | |
# Checks if it is time for the Spawner to spawn or not. | |
# | |
def should_spawn? | |
max = @options[:max] | |
max ? @spawn_count < max && @should_spawn : @should_spawn | |
end | |
# ***** HELPER METHODS ***** | |
private | |
# | |
# Runs the common event if specified. | |
# | |
def run_common_event | |
id = @options[:common_event] | |
@common_event_runner.run(id, self) if id | |
end | |
# | |
# Sets the position that will be used for the next event that | |
# should be spawned. | |
# | |
def set_spawn_pos | |
set_absolute_position if @options[:position] == :absolute | |
set_relative_position if @options[:position] == :relative | |
end | |
# | |
# Sets an absolute position. | |
# | |
def set_absolute_position | |
set_pos(@options[:x], @options[:y]) | |
end | |
# | |
# Shortcut method to set the @x and @y position variables. | |
# | |
def set_pos(x, y) | |
@x = x | |
@y = y | |
end | |
# | |
# Sets a relative position. The position can be relative to the | |
# position of the spawner (up, down etc) or a random position. | |
# | |
def set_relative_position | |
event_x = @game_event.x | |
event_y = @game_event.y | |
x = event_x + @options[:x_adjust] | |
y = event_y + @options[:y_adjust] | |
if @options[:random_pos] | |
set_random_position(event_x, event_y) | |
else | |
if @options[:passable] && !passable?(x, y) | |
set_passable_position(event_x, event_y) | |
else | |
set_pos(x, y) | |
end | |
end | |
end | |
# | |
# Sets @x and @y to passable tile in a one-tile distance from x,y. | |
# If no passable tile exists, sets @x and @y to x,y. | |
# | |
def set_passable_position(x, y) | |
[[y - 1, x], [y + 1, x], [x - 1, y], [x + 1, y]].each do |coord| | |
if passable?(coord[0], coord[1]) | |
@x = coord[0] | |
@y = coord[1] | |
return | |
end | |
end | |
@x, @y = x, y | |
end | |
# | |
# Sets the position to a random coordinate given a start x | |
# and a start y. | |
# | |
def set_random_position(start_x, start_y) | |
distance = @options[:random_pos] | |
coords = get_possible_coords(start_x, start_y, distance).shuffle | |
if @options[:passable] | |
pass = coords.find { |c| passable?(c[0], c[1]) } | |
pass ? set_pos(pass[0], pass[1]) : set_pos(start_x, start_y) | |
else | |
x, y = coords.first | |
set_pos(x, y) | |
end | |
end | |
# | |
# Returns an array of all the coords within distance tiles | |
# of x,y excluding x,y itself. | |
# | |
def get_possible_coords(start_x, start_y, distance) | |
coords = [] | |
left_most = start_x - distance | |
right_most = start_x + distance | |
up_most = start_y - distance | |
down_most = start_y + distance | |
(left_most..right_most).each do |x| | |
(up_most..down_most).each do |y| | |
coords << [x, y] unless x == start_x && y == start_y | |
end | |
end | |
coords | |
end | |
# | |
# Check if a tile is passable on the map. | |
# | |
def passable?(x, y) | |
$game_map.passable?(x, y, 2) | |
end | |
# | |
# Does a deep clone of the given Game_Event object and sets | |
# it's spawn event IDs. | |
# | |
def clone_event(event) | |
id = generate_valid_id | |
cloned_event = Marshal.load(Marshal.dump(event)) | |
cloned_event.set_ids_es_kal(id) | |
cloned_event.set_spawn_name_es_kal(@game_event, id) | |
cloned_event | |
end | |
# | |
# Finds a Game_Event object given an event name. | |
# Searches for events on the current map and the spawn map. | |
# Raises an exception if the event could not be found. | |
# | |
def event_from_name(name) | |
events = $game_map.events.values + (@spawn_map_events || []) | |
event = events.find { |e| e.event.name == name } | |
event || raise(SpawnError, event_name_not_found_msg(name)) | |
end | |
# | |
# Returns an error message if an event could not be found. | |
# identifier is either the Game_Event's id or name. | |
# | |
def event_name_not_found_msg(name) | |
"Event with name '#{name}' could not be found on the current map " + | |
"(ID = #{$game_map.map_id})" + | |
(SPAWN_MAP_ID ? ", or on the spawn map (ID = #{SPAWN_MAP_ID})." : ".") | |
end | |
# | |
# Finds a Game_Event object given an event id. | |
# The id is a number (string) with an optional S attached. | |
# The S indicates that the id refers to and event id on the spawn map. | |
# Raises an exception if the event could not be found. | |
# | |
def event_from_id(id) | |
if id_num = id[/^(\d+)[Xx]/, 1] | |
check_spawn_map_set | |
event = @spawn_map_events.find { |e| e.id == id_num.to_i } | |
event || raise(SpawnError, "Event with id #{id_num} could not " + | |
"be found on the spawn map (ID = #{SPAWN_MAP_ID})") | |
else | |
event = $game_map.events.values.find { |e| e.id == id.to_i } | |
event || raise(SpawnError, "Event with id #{id} could not " + | |
"be found on the current map (ID = #{$game_map.map_id})") | |
end | |
end | |
def check_spawn_map_set | |
SPAWN_MAP_ID || raise(SpawnError, "Tried to spawn with a spawn map " + | |
"event ID, but SPAWN_MAP_ID is not set. Set it in the script CONFIG.") | |
end | |
# | |
# Generates a valid id based on the number of events already on the map. | |
# | |
def generate_valid_id | |
$game_map.events.keys.max + 1 | |
end | |
# | |
# Returns an array with all the Game_Events located on the spawn map. | |
# | |
def load_spawn_map_events | |
id = SPAWN_MAP_ID | |
if id.is_a?(Numeric) && id > 0 | |
begin | |
map = load_data(sprintf("Data/Map%03d.rvdata2", id)) | |
rescue Errno::ENOENT => e | |
raise LoadError, "ERROR: Could not find a map with ID #{id}\n" + | |
"(SPAWN_EVENT_ID = #{id})" | |
end | |
this_map_id = $game_map.map_id | |
map.events.values.map { |event| Game_Event.new(this_map_id, event) } | |
end | |
end | |
end | |
# | |
# LoopSpawner is a spawner that will check if any of the events it | |
# spawned are erased, and if so it will decrement the @spawn_count | |
# variable for that event. | |
# | |
class LoopSpawner < Spawner | |
def update | |
super | |
check_dead_events | |
end | |
def check_dead_events | |
alive = $game_map.events.values.select do |game_event| | |
game_event.event.name =~ /spawn:#{@game_event.event.id}/ && | |
!game_event.erased | |
end | |
@spawn_count = alive.size | |
end | |
end | |
# | |
# Class that makes it easy to execute a common from the map scene. | |
# | |
class CommonEventRunner | |
# | |
# Runs the common event with the given ID and passes in the spawner | |
# to the internal fiber object. | |
# | |
def run(common_event_id, spawner) | |
page = make_page(common_event_id) | |
setup_event(page, spawner) | |
end | |
private | |
# | |
# Returns an event page for a given common event id. | |
# | |
def make_page(id) | |
page = RPG::Troop::Page.new | |
# 117 => common event code, 0 => indent, id => id of common event | |
page.list.unshift(RPG::EventCommand.new(117, 0, [id])) | |
page | |
end | |
# | |
# A big hack in order to not having to change the setup_event method | |
# in the Game_Interpreter class. | |
# | |
def setup_event(page, spawner) | |
$game_map.interpreter.instance_eval do | |
clear | |
@map_id = $game_map.map_id | |
@event_id = 0 | |
@list = page.list | |
@fiber = SpawnerFiberEsKal.new(spawner) { run } if @list | |
end | |
end | |
end | |
end # end EventSpawn | |
end # end Kal | |
class Game_Event | |
attr_accessor :spawner_es_kal, :life_es_kal | |
attr_reader :event, :erased, :spawned_info_es_kal | |
# | |
# Overrides initialize to create a Spawner instance for this event | |
# (or nil if it is not a spawner event). | |
# | |
alias_method :initialize_es_kal, :initialize | |
def initialize(*args) | |
initialize_es_kal(*args) | |
@spawner_es_kal = Kal::EventSpawn::Factory.make_spawner(self, @event) | |
end | |
# | |
# Updates the Spawner and the spawned event's life (if applicable). | |
# | |
alias_method :update_es_kal, :update | |
def update | |
update_es_kal | |
@spawner_es_kal.update if @spawner_es_kal | |
update_life_es_kal | |
end | |
# | |
# Checks the spawned event's life and erases it if life becomes zero. | |
# | |
def update_life_es_kal | |
if @life_es_kal | |
@life_es_kal -= 1 | |
erase if @life_es_kal <= 0 | |
end | |
end | |
# | |
# Sets IDs. | |
# | |
def set_ids_es_kal(id) | |
@event.id = id | |
@id = id | |
end | |
# | |
# Sets the event's name. | |
# | |
def set_spawn_name_es_kal(game_event, id) | |
@event.name = "#{@event.name} spawn:#{game_event.event.id}:#{id}" | |
end | |
end | |
class Game_Map | |
attr_reader :map_id | |
# | |
# Gets all Spawner events on the map that should spawn and spawn them. | |
# | |
alias_method :update_events_es_kal, :update_events | |
def update_events | |
need_to_spawn = @events.values.select do |event| | |
event.spawner_es_kal && event.spawner_es_kal.should_spawn? | |
end | |
need_to_spawn.each { |event| event.spawner_es_kal.spawn } | |
update_events_es_kal | |
end | |
end | |
class SpawnerFiberEsKal < Fiber | |
attr_accessor :spawner | |
def initialize(spawner) | |
@spawner = spawner | |
super() | |
end | |
end | |
class Game_Interpreter | |
# | |
# Checks to see if the current fiber is a spawner fiber when this | |
# method is called. If so, the new game interpreter is setup to also | |
# use a spawner fiber with the same spawner object. This fiber is | |
# accessible from the common event via @fiber. | |
# | |
alias_method :command_117_es_kal, :command_117 | |
def command_117 | |
if @fiber.is_a?(SpawnerFiberEsKal) | |
common_event = $data_common_events[@params[0]] | |
if common_event | |
child = Game_Interpreter.new(@depth + 1) | |
fiber = @fiber | |
child.instance_eval do | |
clear | |
@map_id = $game_map.map_id | |
@event_id = same_map? ? @event_id : 0 | |
@list = common_event.list | |
@fiber = SpawnerFiberEsKal.new(fiber.spawner) { run } if @list | |
end | |
child.run | |
end | |
else | |
command_117_es_kal | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
that's exactly what i looking for!
thanks, it's awesome :)