package net.alcuria.umbracraft.engine.components;
import net.alcuria.umbracraft.Game;
import net.alcuria.umbracraft.definitions.npc.ScriptDefinition;
import net.alcuria.umbracraft.definitions.npc.ScriptPageDefinition;
import net.alcuria.umbracraft.engine.entities.Entity;
import net.alcuria.umbracraft.engine.events.Event;
import net.alcuria.umbracraft.engine.events.EventListener;
import net.alcuria.umbracraft.engine.events.KeyDownEvent;
import net.alcuria.umbracraft.engine.events.ScriptEndedEvent;
import net.alcuria.umbracraft.engine.events.ScriptStartedEvent;
import net.alcuria.umbracraft.engine.scripts.ConditionalCommand;
import net.alcuria.umbracraft.engine.scripts.ScriptCommand;
import net.alcuria.umbracraft.engine.scripts.ScriptCommand.CommandState;
import net.alcuria.umbracraft.util.StringUtils;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
/** A component for handling scripted events, such as cutscenes. Consists of a
* {@link ScriptPageDefinition} that is read and updated accordingly.
* @author Andrew Keturi */
public class ScriptComponent implements Component, EventListener {
public static final String[] LOCAL_FLAGS = { "S1", "S2", "S3" };
private boolean active = false;
private final Rectangle collisionRect = new Rectangle();
private final Array<ScriptCommand> commandStack = new Array<ScriptCommand>();
private ScriptPageDefinition currentPage;
private Entity entity;
private boolean pressed = false, collided = false;
private ScriptDefinition script;
private final Vector3 source = new Vector3();
public ScriptComponent(String scriptId) {
if (scriptId != null) {
script = Game.db().script(scriptId);
} else {
Game.error("Script ID is null. Cannot attach component.");
}
}
@Override
public void create(final Entity entity) {
this.entity = entity;
setCurrentPage(entity);
Game.publisher().subscribe(this);
}
@Override
public void dispose(Entity entity) {
Game.publisher().unsubscribe(this);
}
@Override
public void onEvent(Event event) {
if (event instanceof KeyDownEvent) {
pressed = true;
source.set(((KeyDownEvent) event).source);
} else if (event instanceof FlagChangedEvent && entity != null && ((FlagChangedEvent) event).source != entity) {
setCurrentPage(entity);
}
}
@Override
public void render(Entity entity) {
}
/** Call when some other entity collides with this entity */
public void setCollided() {
collided = true;
}
private void setCurrentPage(Entity entity) {
if (script == null) {
return;
}
ScriptPageDefinition oldPage = currentPage;
// go thru the pages in reverse, finding the first page that has its preconditions met
for (int i = script.pages.size - 1; i >= 0; i--) {
String precondition = script.pages.get(i).precondition;
for (String local : LOCAL_FLAGS) {
if (StringUtils.isNotEmpty(precondition) && precondition.equals(local)) {
precondition = precondition + entity.getId();
}
}
if (precondition == null || !StringUtils.isNotEmpty(precondition) || Game.flags().isSet(precondition)) {
currentPage = script.pages.get(i);
break;
}
}
// if the current page is the same as the last, we don't want to do anything
if (currentPage == oldPage) {
Game.debug("Page did not change, ignoring setCurrentPage for entity: " + entity.getName());
return;
}
// update the animation/animationGroup components
if (currentPage != null && (currentPage.animationCollection != null || currentPage.animationGroup != null || currentPage.animation != null)) {
entity.removeComponent(AnimationComponent.class);
entity.removeComponent(AnimationGroupComponent.class);
entity.removeComponent(AnimationCollectionComponent.class);
if (StringUtils.isNotEmpty(currentPage.animationCollection)) {
entity.addComponent(new AnimationCollectionComponent(Game.db().animCollection(currentPage.animationCollection)));
} else if (StringUtils.isNotEmpty(currentPage.animationGroup)) {
entity.addComponent(new AnimationGroupComponent(Game.db().animGroup(currentPage.animationGroup)));
} else if (StringUtils.isNotEmpty(currentPage.animation)) {
entity.addComponent(new AnimationComponent(Game.db().anim(currentPage.animation)));
}
}
}
/** Immediately marks the script as inactive. Careful with this. */
public void setInactive() {
active = false;
Game.publisher().publish(new ScriptEndedEvent(currentPage));
}
/** Starts a script. should only be called once at the start */
private void startScript() {
if (currentPage.haltInput) {
// halt player movement
Game.publisher().publish(new ScriptStartedEvent(currentPage));
final Entity player = Game.entities().find(Entity.PLAYER);
if (player != null) {
player.velocity.set(0, 0, 0);
}
}
// add the page's command to the stack
commandStack.clear();
commandStack.add(currentPage.command);
commandStack.get(0).setState(CommandState.NOT_STARTED);
active = true;
pressed = false;
}
/** Checks if two entities are in close proximity, using the source vector.
* @param entity the {@link Entity}
* @return true if the vector is close */
private boolean touching(Entity entity) {
pressed = false;
if (MathUtils.isEqual(source.z, entity.position.z)) {
MapCollisionComponent collision = entity.getComponent(MapCollisionComponent.class);
if (collision != null) {
collisionRect.set(entity.position.x - collision.getWidth() / 2, entity.position.y - collision.getHeight() / 2, collision.getWidth(), collision.getHeight());
return source != null && collisionRect.contains(source.x, source.y);
}
}
return false;
}
@Override
public void update(Entity entity) {
// if we have some pages to execute, see if we can do that
if (currentPage == null) {
return;
}
if (!active) {
switch (currentPage.trigger) {
case INSTANT:
startScript();
break;
case ON_INTERACTION:
if (pressed && touching(entity)) {
startScript();
}
break;
case ON_TOUCH:
if (collided) {
startScript();
collided = false;
}
default:
}
} else {
collided = false;
updateScript(entity);
}
}
/** Updates the script, assumes all preconditions are met. (Eg., key has been
* pressed, etc.) */
private void updateScript(Entity entity) {
// ensure we have commands to execute
if (commandStack.size > 0) {
final ScriptCommand current = commandStack.get(commandStack.size - 1);
// get a reference to the top of the stack
if (current != null) {
switch (current.getState()) {
case COMPLETE:
Game.debug("Completing " + current.getName());
final ScriptCommand next = current.getNext();
if (next != null) {
next.setState(CommandState.NOT_STARTED);
}
commandStack.set(commandStack.size - 1, next);
Game.debug(" inserted, new size is " + commandStack.size);
Game.debug(" Next is " + (next != null ? next.getName() : "null"));
if (current instanceof ConditionalCommand) {
final ConditionalCommand cond = (ConditionalCommand) current;
if (cond.isNextNested()) {
// we're inside a block so let's push it to the command stack
commandStack.add(cond.getCalculated());
cond.getCalculated().setState(CommandState.NOT_STARTED);
Game.debug(" Inserting for cond: " + (cond.getCalculated().getName()));
Game.debug(" new size is " + commandStack.size);
}
}
break;
case NOT_STARTED:
Game.debug("Starting " + current.getName());
current.start(entity);
break;
case STARTED:
current.update();
break;
default:
break;
}
}
}
// check if we're done with all scripts. stack size changes above so we can't dump this in the else.
if (commandStack.size < 1) {
setInactive();
setCurrentPage(entity);
} else if (commandStack.get(commandStack.size - 1) == null) {
commandStack.pop();
}
}
}