//this code is licensed under a Creative Commons Attribution-Share Alike license
//see http://creativecommons.org/licenses/by-sa/3.0/ for details
package {
import flash.display.Sprite;
import flash.utils.Dictionary;
import flash.utils.ByteArray;
import flash.utils.getTimer;
import flash.text.*;
import flash.events.*;
import flash.geom.Point;
public class Level extends Sprite {
//all variables are defined above the methods in which they are used
//sets up the level and stage
public function Level() {
stage.showDefaultContextMenu = false;
//stage.frameRate = 10;
createDictionary();
createLevelHolder();
createInput();
createOutput();
setupFocus();
registerEvents();
createLevel("5|5|i000I0000000p0000000T000t");
//"7|7|p000000000000000trT0000bgb0000Iri0000000000000000");
}
////////========Physics========////////
private var paused:Boolean = true;
private var initialX:Number, initialY:Number;
//resets the player and all physics-related variables
private function resetLevel():void {
yVel = 0;
xVel = 0;
if(player != null) {
player.x = initialX;
player.y = initialY;
}
}
private var xVel:Number = 0, yVel:Number = 0;
//runs the physics for the given time in milliseconds
private function runPhysics(time:int):void {
if(player == null || paused) return;
//set the velocity based on user input
//yVel += .0001*blockSize*time;
if(isDown(37)) {
xVel -= .0001*blockSize*time;
}
if(isDown(39)) {
xVel += .0001*blockSize*time;
}
if(isDown(40)) {
yVel += .0001*blockSize*time;
}
if(isDown(38)) {
yVel -= .0001*blockSize*time;
}
xVel *= 0.99;
yVel *= 0.99;
//find the farthest the player can move
var newPos:Point = new Point(player.x + xVel, player.y + yVel),
collisionData:CollisionData = new CollisionData(newPos, null);
//while(newPos.x != player.x || newPos.y != player.y) {
for(var r:int = 0; r < level.length; r++) {
for(var c:int = 0; c < level[0].length; c++) {
collisionData = collide(r, c, collisionData);
}
}
collisionData = collide(-1, -1, collisionData);
//if the player hit anything, update its velocity
if(collisionData.moveTo) {
/*if(collisionData.objectHit is LineSegment) {
var lineHit:LineSegment = (LineSegment)(collisionData.objectHit),
slope1:Number = lineHit.slope, slope2:Number = -1/slope1,
onePoint:Point = lineHit.start;
if(slope1 == Infinity) {
//TODO
} else if(slope1 == 0) {
//TODO
} else {
//use a formula for finding the projection of one vector onto another
xVel = (slope1*onePoint.x + slope2*newPos.x + newPos.y - onePoint.y)
/ (slope1 + slope2);
yVel = slope1*(xVel - onePoint.x);
xVel -= onePoint.x;
}
}*/
newPos = collisionData.moveTo;
xVel = newPos.x - player.x;
yVel = newPos.y - player.y;
player.x = newPos.x;
player.y = newPos.y;
/*newPos.x += xVel;
newPos.y += yVel;*/
} else {
player.x = newPos.x;
player.y = newPos.y;
//break;
}
//}
}
//checks whether the player would touch the item at position (r, c)
//if moved to newPos and returns the farthest point that
//the player can move to without collision
private function collide(r:int, c:int,
lastCollision:CollisionData):CollisionData {
var newPos:Point = lastCollision.hitLocation, farthest:Point = newPos;
//if the player can't move, no more testing is necessary
if(player.x == newPos.x && player.y == newPos.y)
return(lastCollision);
//get the object data and stop if there is no object
var ob:Block = (r >= 0 ? level[r][c] : background),
points:Array = null, halfSize:Number = blockSize * 0.5;
if(ob == null || ob is Player)
return(lastCollision);
points = ob.points;
//declare variables to be used in the loop here
var deltaX:Number, deltaY:Number,
prevPoint:Point = points[points.length-1],
lineStart:Point, lineEnd:Point, collision:Point,
objectHit:Object = lastCollision.objectHit,
moveTo:Point = lastCollision.moveTo;
//update prevPoint to reflect its position on the stage
prevPoint = new Point(prevPoint.x + ob.x, prevPoint.y + ob.y);
//iterate through all the points and check for collisions
//both with the points and the lines between them
for each(var p:Point in points) {
//update p to reflect its position on the stage
p = new Point(p.x + ob.x, p.y + ob.y);
//hittest the point
//TODO: add a point hittest
//hittest the line
//get the horizontal/vertical distance travelled
deltaX = newPos.x - player.x;
deltaY = newPos.y - player.y;
//find the path of the point on the circle
//that would hit the current line first
lineStart = radialPoint(p, prevPoint);
lineEnd = new Point(lineStart.x + deltaX, lineStart.y + deltaY);
//check to see if the point actually hit
collision = segmentIntersection(lineStart, lineEnd, prevPoint, p);
if(collision != null) {
collision.x += player.x - lineStart.x;
collision.y += player.y - lineStart.y;
if(Math.abs(farthest.x - player.x) > Math.abs(collision.x - player.x)
|| Math.abs(farthest.y - player.y) > Math.abs(collision.y - player.y)) {
farthest = collision;
objectHit = new LineSegment(lineStart, lineEnd);
moveTo = new Point(farthest.x - (lineStart.x - player.x) * 0.002,
farthest.y - (lineStart.y - player.y) * 0.002);
}
} else {
//TODO: check if the player is overlapping the triangle
//and output data if so
}
prevPoint = p;
}
return(new CollisionData(farthest, objectHit, moveTo));
}
//returns the point on the circle at the end of the radius perpendicular to the given line segment
private function radialPoint(p1:Point, p2:Point):Point {
var radius:Number = blockSize/2;
//check to see if the line is horizontal or vertical
//and, if so, return the appropriate point
if(p1.x == p2.x) {
return(new Point(player.x
+ ((player.x < p1.x) ? radius : -radius), player.y));
}
if(p1.y == p2.y) {
return(new Point(player.x, player.y
+ ((player.y < p1.y) ? radius : -radius)));
}
//find the slope of the radius perpendicular to the line
var rSlope:Number = -(p2.x - p1.x) / (p2.y - p1.y),
//find and return the point at the end
rPoint:Point = new Point(0, 0);
rPoint.x = player.x + Math.sqrt(radius*radius / (rSlope*rSlope + 1))
* ((rSlope < 0) != ((p1.x-player.x) / rSlope + p1.y < player.y) ? -1 : 1);
rPoint.y = rSlope * (rPoint.x - player.x) + player.y;
return(rPoint);
}
//draws a box around the given point
private function displayPoint(p:Point):void {
with(visualOutput.graphics) {
lineStyle(1);
moveTo(p.x + 1, p.y + 1);
lineTo(p.x - 1, p.y + 1);
lineTo(p.x - 1, p.y - 1);
lineTo(p.x + 1, p.y - 1);
lineTo(p.x + 1, p.y + 1);
}
}
//draws the given line
private function displayLine(p1:Point, p2:Point):void {
with(visualOutput.graphics) {
lineStyle(1, 0xAAAAAA);
moveTo(p1.x, p1.y);
lineTo(p2.x, p2.y);
}
}
//gets the point of intersection the line segments
//defined by p11->p12 and p21->p22
//or returns null if they don't intersect
private function segmentIntersection(p11:Point, p12:Point,
p21:Point, p22:Point):Point {
//get the slopes of the two lines
var slope1:Number = (p12.y - p11.y) / (p12.x - p11.x),
slope2:Number = (p22.y - p21.y) / (p22.x - p21.x);
//stop if the lines are parallel
if(slope1 == slope2 || p11.x == p12.x && p21.x == p22.x)
return(null);
var intersection:Point;
//check if either line is vertical or near vertical
if(Math.abs(slope1) > 100) {
intersection = new Point(p11.x, slope2 * (p11.x - p21.x) + p21.y);
} else if(Math.abs(slope2) > 100) {
intersection = new Point(p21.x, slope1 * (p21.x - p11.x) + p11.y);
//check if either line is horizontal
} else {
//use a formula for the intersection of non-vertical lines
//find the x coordinate of the intersection
intersection = new Point((slope1*p11.x - slope2*p21.x + p21.y - p11.y)
/ (slope1 - slope2), 0);
//use the x coordinate to find y
intersection.y = slope1 * (intersection.x - p11.x) + p11.y;
}
//return the intersection between the lines if it is also on both segments
if(isBetween(intersection.x, p11.x, p12.x)
&& isBetween(intersection.x, p21.x, p22.x)
&& isBetween(intersection.y, p11.y, p12.y)
&& isBetween(intersection.y, p21.y, p22.y))
return(intersection);
return(null);
}
//checks if the first value is between the other two
private function isBetween(v1:Number, v2:Number, v3:Number):Boolean {
//add or subtract 0.000001 to compensate for rounding errors
if(v2 < v3)
return(v2 - 0.000001 < v1 && v1 < v3 + 0.000001);
return(v3 - 0.000001 < v1 && v1 < v2 + 0.000001);
}
////////========Level creation========////////
private var level:Array;
private var parser:RegExp = /(\d+)\|(\d+)\|([a-zA-Z0-9]+)/;
private var blockSize:int = 50;
private var player:Player, levelHolder:Sprite, background:Block;
//parses the given string and builds a level from it
private function createLevel(data:String):void {
//parse the data
var match:Object = parser.exec(data);
if(match == null || match[0] == input.text)
return;
//unload the previous level, if there was one
if(level != null) {
for each(var a:Array in level)
for each(var cur:Block in a)
try {
levelHolder.removeChild(cur);
} catch(e:Error) {}
try {
levelHolder.removeChild(player);
} catch(e:Error) {}
player = null;
}
//iterate through the data, placing blocks at the appropriate places
var curBlock:Block, curLetter:String,
height:int = parseInt(match[1]), width:int = parseInt(match[2]);
level = new Array(height);
data = match[3];
for(var r:int = 0; r < height; r++) {
level[r] = new Array(width);
for(var c:int = 0; c < width; c++) {
curLetter = data.charAt(r*width + c);
if(curLetter in f) {
curBlock = f[curLetter]();
} else {
curBlock = null;
}
if(curBlock != null) {
curBlock.x = (c + 0.5)*blockSize;
curBlock.y = (r + 0.5)*blockSize;
levelHolder.addChild(curBlock);
}
level[r][c] = curBlock;
}
}
if(player != null) {
//record the player's initial position
initialX = player.x;
initialY = player.y;
}
//update the background to match the current level size
background.points[2].x = background.points[3].x = blockSize * width;
background.points[1].y = background.points[2].y = blockSize * height;
background.redraw();
//output the current data, adding zeros to the end if the string isn't long enough
var extraZeros:String = "", blankSpaces:int = width*height - data.length;
if(blankSpaces < 0) {
input.text = match[0].substring(0, match[0].length + blankSpaces);
} else {
for(; blankSpaces > 0; blankSpaces--) {
extraZeros += "0";
}
input.text = match[0] + extraZeros;
}
}
//return null to indicate a blank space
private function blank():Block {
return(null);
}
//create squares of the given colors
private function graySquare():Square {
return(new Square(blockSize));
}
private function redSquare():Square {
return(new Square(blockSize, 0xFF0000));
}
private function greenSquare():Square {
return(new Square(blockSize, 0x00FF00));
}
private function blueSquare():Square {
return(new Square(blockSize, 0x0000FF));
}
//create triangles facing in various directions
private function triangle0():Triangle {
return(new Triangle(blockSize, 0));
}
private function triangle1():Triangle {
return(new Triangle(blockSize, 1));
}
private function triangle2():Triangle {
return(new Triangle(blockSize, 2));
}
private function triangle3():Triangle {
return(new Triangle(blockSize, 3));
}
//creates a circle to serve as the player
private function makePlayer(color:int = 0x5555FF, lineColor:int = 0x000000):Player {
if(player == null)
player = new Player(blockSize, color, lineColor);
return(player);
}
private var f:Dictionary = new Dictionary();
//creates the dictionary mapping letters to block types
private function createDictionary():void {
f["0"] = blank;
f["d"] = graySquare;
f["r"] = redSquare;
f["g"] = greenSquare;
f["b"] = blueSquare;
f["p"] = makePlayer;
f["t"] = triangle0;
f["T"] = triangle1;
f["i"] = triangle2;
f["I"] = triangle3;
}
////////========Interaction========////////
//sets up the events for movement
private function registerEvents():void {
addEventListener("enterFrame", onEnterFrame);
keysPressed.position = 100;
keysPressed.writeBoolean(false);
addEventListener("keyDown", onKeyDown);
addEventListener("keyUp", onKeyUp);
levelHolder.addEventListener("mouseDown", onMouseDown);
levelHolder.addEventListener("mouseMove", onMouseMove);
}
private var focusHolder:Sprite;
//sets up the focus handling
private function setupFocus():void {
focusHolder = new Sprite();
addChild(focusHolder);
}
private var lastFrame:int = 0;
//makes regular focus checks
//applies the physics
private function onEnterFrame(e:Event):void {
if(stage.focus == null)
stage.focus = focusHolder;
if(isDown(13) && stage.focus == input)
onSubmit(new Event(""));
runPhysics(getTimer() - lastFrame);
lastFrame = getTimer();
}
private var keysPressed:ByteArray = new ByteArray();
//handles key down events
private function onKeyDown(e:KeyboardEvent):void {
keysPressed.position = e.keyCode;
keysPressed.writeBoolean(true);
}
//handles key up events
private function onKeyUp(e:KeyboardEvent):void {
keysPressed.position = e.keyCode;
keysPressed.writeBoolean(false);
}
//checks if the given key is down
private function isDown(code:uint):Boolean {
keysPressed.position = code;
return(keysPressed.readBoolean());
}
private var s:Point = null, end:Point = null;
//updates the value of add based on whether the mouse is over a tile
private function onMouseDown(e:MouseEvent):void {
onMouseMove(e, true);
}
private var add:Boolean = true, curType:String = "d";
//adds or removes the block at the mouse's location
private function onMouseMove(e:MouseEvent, justPressed:Boolean = false):void {
if(!e.buttonDown) {
paused = false;
return;
}
resetLevel();
paused = true;
var c:int = int(levelHolder.mouseX / blockSize);
var r:int = int(levelHolder.mouseY / blockSize);
if(r < 0 || r >= level.length || c < 0 || c >= level[0].length) {
return;
}
if(justPressed)
add = level[r][c] == null;
//define variables here to avoid duplicate variable definitions
var secondBar:int, charPos:int, newString:String;
if(level[r][c] != null) {
if(!add) {
//remove the block
try {
levelHolder.removeChild(level[r][c]);
} catch(e:Error) {}
if(level[r][c] == player)
player = null;
level[r][c] = null;
//update the input field string
secondBar = input.text.indexOf("|", input.text.indexOf("|") + 1);
charPos = secondBar + r*level[0].length + c + 1;
newString = input.text;
newString = newString.substring(0, charPos) + "0" + newString.substring(charPos + 1);
input.text = newString;
}
} else {
if(add) {
//add a new block
var curBlock:Block = f[curType]();
curBlock.x = (c + 0.5) * blockSize;
curBlock.y = (r + 0.5) * blockSize;
levelHolder.addChild(curBlock);
level[r][c] = curBlock;
//update the input field string
secondBar = input.text.indexOf("|", input.text.indexOf("|") + 1);
charPos = secondBar + r*level[0].length + c + 1;
newString = input.text;
newString = newString.substring(0, charPos) + "d" + newString.substring(charPos + 1);
input.text = newString;
}
}
}
////////========Input and output========////////
private var input:TextField, output:TextField, visualOutput:Sprite;
//sets up the input text field
private function createInput():void {
if(input != null) return;
input = new TextField();
input.height = 50;
input.width = 400;
input.x = 33;
input.y = 350;
input.border = true;
input.addEventListener("focusOut", onSubmit);
input.type = "input";
input.wordWrap = true;
input.restrict = "0-9\\|a-zA-Z";
addChild(input);
}
//sets up the output text field that is used for the trace() function
private function createOutput():void {
if(output != null) return;
output = new TextField();
output.text = "Output:\n";
output.height = 64;
output.width = 400;
output.x = 33;
output.y = 400;
output.multiline = true;
output.mouseWheelEnabled = true;
addChild(output);
visualOutput = new Sprite();
addChild(visualOutput);
}
//creates the level holder and a white square to define its boundaries
private function createLevelHolder():void {
levelHolder = new Sprite();
addChild(levelHolder);
background = new Block(new Array(new Point(0, 0),
new Point(0, 1),
new Point(1, 1),
new Point(1, 0)));
levelHolder.addChild(background);
background.alpha = 0;
}
//reloads the level when the user clicks out of the text field
private function onSubmit(e:Event):void {
createLevel(input.text);
}
//outputs the given data
private function trace(... r):void {
//convert all given objects to strings and concatenate them
var string:String = "";
for each(var o:Object in r) {
if(o == null)
string += "null, ";
else
string += o.toString() + ", ";
}
string = string.substring(0, string.length-2);
//add line numbers to the beginning of all new lines in the string
var numLines:int = output.numLines - 1;
var toPrint:String = "";
var lines:Array = string.split("\n");
for each(var s:String in lines) {
toPrint += numLines.toString() + ": " + s + "\n";
numLines++;
}
//append the text and scroll a very large number of lines down (it stops at the end)
output.appendText(toPrint);
output.scrollV = numLines;
}
}
}
import flash.display.Sprite;
import flash.geom.Point;
class Block extends Sprite {
public var points:Array;
private var color:int, lineColor:int;
public function Block(inputPoints:Array = null,
color:int = 0x888888, lineColor:int = 0x000000) {
if(inputPoints == null || inputPoints.length < 2) {
//leave a blank space
points = null;
} else {
points = inputPoints;
this.color = color;
this.lineColor = lineColor;
redraw();
}
}
public function redraw():void {
graphics.clear();
graphics.lineStyle(1, lineColor);
graphics.beginFill(color);
graphics.moveTo(points[0].x, points[0].y);
for(var i:int = 1; i < points.length; i++) {
graphics.lineTo(points[i].x, points[i].y);
}
graphics.lineTo(points[0].x, points[0].y);
graphics.endFill();
}
}
class Square extends Block {
public function Square(size:int = 1,
color:int = 0x888888, lineColor:int = 0x000000) {
var halfSize:Number = size / 2;
points = new Array();
points[0] = new Point(-halfSize, -halfSize);
points[1] = new Point(halfSize, -halfSize);
points[2] = new Point(halfSize, halfSize);
points[3] = new Point(-halfSize, halfSize);
super(points, color, lineColor);
}
}
class Triangle extends Block {
public function Triangle(size:int = 1, direction:int = 0,
color:int = 0x888888, lineColor:int = 0x000000) {
var halfSize:Number = size / 2;
points = new Array();
points[0] = new Point(-halfSize, -halfSize);
points[1] = new Point(halfSize, -halfSize);
points[2] = new Point(halfSize, halfSize);
points[3] = new Point(-halfSize, halfSize);
try {
points.splice(direction, 1);
} catch(e:Error) {
points.splice(0, 1);
}
super(points, color, lineColor);
}
}
class Player extends Block {
public function Player(size:int = 1,
color:int = 0x888888, lineColor:int = 0x000000) {
graphics.lineStyle(1, lineColor);
graphics.beginFill(color);
graphics.drawCircle(0, 0, size/2);
graphics.endFill();
//don't have the Block class do anything
super();
}
}
class CollisionData {
public var hitLocation:Point, objectHit:Object, moveTo:Point;
public function CollisionData(hitLoc:Point, obHit:Object = null, mTo:Point = null) {
hitLocation = hitLoc;
objectHit = obHit;
moveTo = mTo;
}
}
class LineSegment {
private var p1:Point, p2:Point, m:Number;
public function LineSegment(s:Point, e:Point) {
p1 = s;
p2 = e;
if(p1.x == p2.x)
m = Infinity;
else
m = (p2.y - p1.y) / (p2.x - p1.x);
}
public function get start():Point {
return(p1);
}
public function get end():Point {
return(p2);
}
public function get slope():Number {
return(m);
}
}