Skip to content

Instantly share code, notes, and snippets.

@kl
Last active September 30, 2015 12:28
Show Gist options
  • Save kl/1790186 to your computer and use it in GitHub Desktop.
Save kl/1790186 to your computer and use it in GitHub Desktop.
Event Spawn for VX Ace 1.2
# 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
@wasteland540
Copy link

that's exactly what i looking for!
thanks, it's awesome :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment