// An attempt to generate corridors for a dungeon-like map // Copyright 2008 Amit J. Patel // License: MIT package { import flash.display.*; import flash.geom.*; import flash.events.*; import flash.utils.*; [SWF(width="500", height="500", frameRate="60", backgroundColor="#dddddd")] public class corridor extends Sprite { public var c:Corridor; public var overlay:Sprite = new Sprite(); public var playerOverlay:Sprite = new Sprite(); public var debugOverlay:Sprite = new Sprite(); public var debugLayerOn:Boolean = false; public var animationOn:Boolean = false; /* The alternate coordinate system is (u,v) where u is the * location along the corridor and v is perpendicular to the * corridor. TODO: v locations not implemented. */ public var playerLocationU:Number = 0.0; public var playerTargetU:Number = 10.5; public var playerLocationV:Number = 0.2; public var playerTargetV:Number = 0.5; function corridor() { addChild(new Debug(this)); addChild(overlay); addChild(debugOverlay); addChild(playerOverlay); createCorridor(); var timer:Timer = new Timer(1000/30, 0); timer.addEventListener(TimerEvent.TIMER, timerEvent); addEventListener(MouseEvent.CLICK, onClick); redraw(); var toggle0:SimpleButton = new SimpleButton(); toggle0.upState = new ButtonShape("New Corridor", 0xccbbdd, 0); toggle0.downState = new ButtonShape("New Corridor", 0xffbbff, 1); toggle0.overState = new ButtonShape("New Corridor", 0xddbbcc, 0); toggle0.hitTestState = toggle0.upState; toggle0.addEventListener(MouseEvent.CLICK, function():void { createCorridor(); redraw(); } ); addChild(toggle0); var toggle1:SimpleButton = new SimpleButton(); toggle1.upState = new ButtonShape("Toggle Animation", 0xccbbdd, 0); toggle1.downState = new ButtonShape("Toggle Animation", 0xffbbff, 1); toggle1.overState = new ButtonShape("Toggle Animation", 0xddbbcc, 0); toggle1.hitTestState = toggle1.upState; toggle1.addEventListener(MouseEvent.CLICK, function():void { animationOn = !animationOn; if (animationOn) timer.start(); else timer.stop(); } ); toggle1.x = toggle0.x + toggle0.width + 10; addChild(toggle1); var toggle2:SimpleButton = new SimpleButton(); toggle2.upState = new ButtonShape("Toggle Debug", 0xccbbdd, 0); toggle2.downState = new ButtonShape("Toggle Debug", 0xffbbff, 1); toggle2.overState = new ButtonShape("Toggle Debug", 0xddbbcc, 0); toggle2.hitTestState = toggle2.upState; toggle2.addEventListener(MouseEvent.CLICK, function():void { debugLayerOn = !debugLayerOn; debugOverlay.graphics.clear(); if (debugLayerOn) c.drawDebug(debugOverlay.graphics); } ); toggle2.x = toggle1.x + toggle1.width + 10; addChild(toggle2); } public function createCorridor():void { c = new Corridor(); for (var i:int = 0; i < 500 / Segment.length; i++) { c.addSegment(); } } public function redraw():void { graphics.clear(); overlay.graphics.clear(); debugOverlay.graphics.clear(); playerOverlay.graphics.clear(); drawBackground(); drawGrid(); c.draw(overlay.graphics, playerLocationU); if (debugLayerOn) c.drawDebug(debugOverlay.graphics); drawPlayer(); } public function onClick(event:MouseEvent):void { for (var i:int = 0; i < c.segments.length-1; i++) { if (Segment(c.segments[i]).containsPoint(new Point(event.localX, event.localY), c.segments[i+1])) { playerTargetU = i + 0.5; } } } public function timerEvent(event:Event):void { c.ripple(); playerLocationU += Math.min(0.1, Math.max(-0.1, playerTargetU - playerLocationU)); playerLocationV += Math.min(0.01, Math.max(-0.01, playerTargetV - playerLocationV)); redraw(); } public function drawPlayer():void { var segmentLocation:int = Math.floor(playerLocationU); var segmentPosition:Number = playerLocationU - segmentLocation; var p:Point = Point.interpolate(Segment(c.segments[segmentLocation]).start, Segment(c.segments[segmentLocation+1]).start, 1.0 - segmentPosition); playerOverlay.graphics.lineStyle(3, 0x000000, 0.5); playerOverlay.graphics.beginFill(0xffffcc, 0.8); playerOverlay.graphics.drawCircle(p.x, p.y, 4); playerOverlay.graphics.endFill(); playerOverlay.graphics.lineStyle(); } // Drawing functions private var _offsets:Array = [ new Point(0, 0), new Point(0, 0), new Point(0, 0) ]; private function drawBackground():void { var seed:int = 17; Point(_offsets[0]).x -= 1; Point(_offsets[0]).y -= 1; Point(_offsets[1]).x += 2; Point(_offsets[2]).y += 2; var bitmapData:BitmapData = new BitmapData(500, 500); bitmapData.perlinNoise(500, 500, 3, seed, true, false, 7, true, _offsets); bitmapData.colorTransform(bitmapData.rect, new ColorTransform(1.1, 1.0, 1.0, 1.0, 48, 32, 64, 0)); var matrix:Matrix = new Matrix(); // matrix.scale(10.0, 10.0); graphics.beginBitmapFill(bitmapData, matrix, false, true); graphics.drawRect(0, 0, 500, 500); graphics.endFill(); } private function drawGrid():void { graphics.lineStyle(0, 0xbbbbaa, 0.3); for (var i:int = 0; i <= 500; i += 10) { graphics.moveTo(0, i); graphics.lineTo(500, i); graphics.moveTo(i, 0); graphics.lineTo(i, 500); } graphics.lineStyle(); } } } import flash.geom.*; import flash.display.*; import flash.text.*; import flash.filters.*; class Segment { public var id:int; // These are independent of any other segments public var leftWidth:Number; public var rightWidth:Number; public var desiredAngle:Number; public static var length:Number = 10; // These are calculated, and represent the allowed change in // angle from the previous segment public var minAngle:Number; public var maxAngle:Number; public var constrainedAngle:Number; public function calculateAngles(prev:Segment, next:Segment):void { var prevAngle:Number = prev? prev.constrainedAngle : Corridor.initialAngle; minAngle = (!next)? 0 : -Math.atan2(length, next.rightWidth); if (length < rightWidth) { minAngle = Math.max(minAngle, -Math.acos(length / rightWidth)); } maxAngle = (!next)? 0 : Math.atan2(length, next.leftWidth); if (length < leftWidth) { maxAngle = Math.min(maxAngle, Math.acos(length / leftWidth)); } // Further constrain the angles minAngle = 0.5*minAngle; maxAngle = 0.5*maxAngle; constrainedAngle = Math.min(maxAngle, Math.max(minAngle, desiredAngle - prevAngle)) + prevAngle; } // These are calculated from the current segment, after constrainedAngle public var forwardVector:Point; public var leftVector:Point; public var start:Point; public function calculateVectors(prev:Segment):void { forwardVector = Point.polar(1, constrainedAngle); leftVector = Point.polar(1, constrainedAngle + Math.PI/2); if (prev) start = prev.start.add(scale(forwardVector, length)); } // These are computed from the previous segment public var left:Point; public var right:Point; public function calculatePoints():void { left = start.add(scale(leftVector, leftWidth)); right = start.add(scale(leftVector, -rightWidth)); } // Helper functions // Returns {u,v} if point is inside, or null otherwise public function containsPoint(p:Point, next:Segment):Object { // We have to check all four sides of the polygon var bottomDot:Number = toTheLeftOf(p, left, right); var rightDot:Number = toTheLeftOf(p, right, next.right); var topDot:Number = toTheLeftOf(p, next.right, next.left); var leftDot:Number = toTheLeftOf(p, next.left, left); // If all four sides are okay, then the coordinates are computed. // NOTE: we want top > 0 instead of >= 0, so that points // on the top line are consider part of the next segment. if (bottomDot >= 0 && rightDot >= 0 && topDot > 0 && leftDot >= 0) { // Debug.trace(" BT ", bottomDot, " ", topDot); // Debug.trace(" LF ", leftDot, " ", rightDot); return true; } return false; } public function distance(a:Point, b:Point):Number { return a.subtract(b).length; } public function toTheLeftOf(p:Point, a:Point, b:Point):Number { var perpendicular:Point = new Point(a.y - b.y, b.x - a.x); var z:Number = perpendicular.x * (p.x-a.x) + perpendicular.y * (p.y-a.y); return z; } public function scale(p:Point, f:Number):Point { return new Point(p.x * f, p.y * f); } } class Corridor { static public var initialAngle:Number = -Math.PI/2; static public var minWidth:Number = 15; static public var rippleWavelength:Number = 30; static public var rippleAmplitude:Number = 0.2; static public var rippleDeltaPhase:Number = 0.1; static public var rippleAngularChange:Number = 0.02; public var segments:Array = []; public var ripplePhase:Number = 0.0; public var fogOfWarDistance:Number = 2; // Perlin noise texture private var _bitmapData:BitmapData = new BitmapData(256, 256); public function Corridor() { var s:Segment = new Segment; s.leftWidth = 30; s.rightWidth = 50; s.desiredAngle = initialAngle; s.start = new Point(250, 500); s.calculateAngles(null, null); s.calculateVectors(null); s.calculatePoints(); segments.push(s); _bitmapData.perlinNoise(256, 256, 9, Math.round(999999*Math.random()), true, false, 7, true); _bitmapData.colorTransform(_bitmapData.rect, new ColorTransform(1.0, 1.0, 1.0, 1.0, 96, 48, 48, 0)); } public function addSegment():void { var prevprev:Segment = (segments.length >= 2)? segments[segments.length-2] : null; var prev:Segment = segments[segments.length-1]; var next:Segment = new Segment(); next.id = segments.length; segments.push(next); next.leftWidth = Math.max(minWidth/2, prev.leftWidth + (Math.random()-0.5)*15); next.rightWidth = Math.max(minWidth/2, prev.rightWidth + (Math.random()-0.5)*15); next.desiredAngle = prev.desiredAngle - 0.5*(Math.random()-0.5); next.calculateAngles(prev, null); next.calculateVectors(prev); next.calculatePoints(); prev.calculateAngles(prevprev, next); prev.calculateVectors(prevprev); prev.calculatePoints(); adjustAllSegments(); } // Adjust all angles and sizes to meet constraints public function adjustAllSegments():void { for (var i:int = 0; i != segments.length; i++) { var s:Segment = segments[i]; s.calculateAngles(i >= 1? segments[i-1] : null, i < segments.length-1? segments[i+1] : null); s.calculateVectors(i >= 1? segments[i-1] : null); s.calculatePoints(); } } // Modify the width of the corridor like a sin wave public function ripple():void { for each (var s:Segment in segments) { var a:Number = ripplePhase + Math.PI * 2 * (s.id / rippleWavelength); var beforeLeft:Number = 1 + rippleAmplitude * Math.sin(a); var afterLeft:Number = 1 + rippleAmplitude * Math.sin(a + rippleDeltaPhase); var beforeRight:Number = 1 + rippleAmplitude * Math.sin(a*0.31); var afterRight:Number = 1 + rippleAmplitude * Math.sin((a + rippleDeltaPhase)*0.31); s.leftWidth *= (afterLeft/beforeLeft); s.rightWidth *= (afterRight/beforeRight); s.desiredAngle += rippleAngularChange * Math.sin(a*0.73); } ripplePhase += rippleDeltaPhase; adjustAllSegments(); } public function draw(g:Graphics, viewPoint:Number):void { g.beginBitmapFill(_bitmapData); g.moveTo(Segment(segments[0]).right.x, Segment(segments[0]).right.y); g.lineStyle(5, 0x000000, 0.2); for (var i:int = 1; i != segments.length; i++) { g.lineTo(Segment(segments[i]).right.x, Segment(segments[i]).right.y); } g.lineStyle(); g.lineTo(Segment(segments[segments.length-1]).left.x, Segment(segments[segments.length-1]).left.y); g.lineStyle(5, 0x000000, 0.2); for (i = segments.length-2; i >= 0; i--) { g.lineTo(Segment(segments[i]).left.x, Segment(segments[i]).left.y); } g.lineStyle(); g.endFill(); // Draw the area that the player can see // TODO: this code is similar to the above, refactor? var first:int = Math.max(0, Math.round(viewPoint-fogOfWarDistance)); var last:int = Math.min(segments.length, Math.round(viewPoint+fogOfWarDistance)); g.beginFill(0xffffff, 0.2); g.moveTo(Segment(segments[first]).right.x, Segment(segments[first]).right.y); for (i = first+1; i < last; i++) { g.lineTo(Segment(segments[i]).right.x, Segment(segments[i]).right.y); } for (i = last-1; i >= first; i--) { g.lineTo(Segment(segments[i]).left.x, Segment(segments[i]).left.y); } g.endFill(); } public function drawDebug(g:Graphics):void { for (var i:int = 0; i != segments.length; i++) { var s:Segment = segments[i]; g.lineStyle(1, 0x000000); g.drawCircle(s.start.x, s.start.y, 2); g.moveTo(s.start.x, s.start.y); g.lineTo(s.start.x + s.forwardVector.x*5, s.start.y + s.forwardVector.y*5); g.lineStyle(1, 0xffffff, 0.1); g.moveTo(s.left.x, s.left.y); g.lineTo(s.right.x, s.right.y); g.moveTo(s.right.x, s.right.y); // cancel fill g.lineStyle(1, 0xff0099); g.beginFill(0xffffff, 0.5); g.drawCircle(s.left.x, s.left.y, 2); g.endFill(); g.lineStyle(1, 0x9900ff); g.beginFill(0xffffff, 0.5); g.drawCircle(s.right.x, s.right.y, 2); g.endFill(); g.lineStyle(); } } } class ButtonShape extends Sprite { function ButtonShape(text: String, color:int, offset:int) { var label:TextField = new TextField(); var format:TextFormat = new TextFormat(); format.font = "_sans"; format.size = 15; label.defaultTextFormat = format; label.text = text; label.selectable = false; label.x = offset + 2; label.y = offset + 2; label.width = label.textWidth + 4; label.height = label.textHeight + 4; addChild(label); graphics.beginFill(color); graphics.drawRect(0, 0, label.textWidth + 8, label.textHeight + 8); graphics.endFill(); filters = [new BevelFilter(1)]; } }