diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee index 20c76ad36..21da1b4b6 100644 --- a/app/lib/surface/Surface.coffee +++ b/app/lib/surface/Surface.coffee @@ -1,5 +1,5 @@ CocoClass = require 'lib/CocoClass' -path = require './path' +TrailMaster = require './TrailMaster' Dropper = require './Dropper' AudioPlayer = require 'lib/AudioPlayer' {me} = require 'lib/auth' @@ -181,7 +181,6 @@ module.exports = Surface = class Surface extends CocoClass framesDropped = 0 while true Dropper.tick() - @trailmaster.tick() if @trailmaster # Skip some frame updates unless we're playing and not at end (or we haven't drawn much yet) frameAdvanced = (@playing and @currentFrame < lastFrame) or @totalFramesDrawn < 2 if frameAdvanced and @playing @@ -210,7 +209,6 @@ module.exports = Surface = class Surface extends CocoClass @updateState @currentFrame isnt oldFrame @drawCurrentFrame e @onFrameChanged() - @updatePaths() if (@totalFramesDrawn % 4) is 0 or createjs.Ticker.getMeasuredFPS() > createjs.Ticker.getFPS() - 5 Backbone.Mediator.publish('surface:ticked', {dt: 1 / @options.frameRate}) mib = @webGLStage.mouseInBounds if @mouseInBounds isnt mib @@ -456,7 +454,8 @@ module.exports = Surface = class Surface extends CocoClass @fastForwardingSpeed = lag / intendedLag else @fastForwardingToFrame = @fastForwardingSpeed = null - #console.log "on new world, lag", lag, "intended lag", intendedLag, "fastForwardingToFrame", @fastForwardingToFrame, "speed", @fastForwardingSpeed, "cause we are at", @world.age, "of", @world.frames.length * @world.dt +# console.log "on new world, lag", lag, "intended lag", intendedLag, "fastForwardingToFrame", @fastForwardingToFrame, "speed", @fastForwardingSpeed, "cause we are at", @world.age, "of", @world.frames.length * @world.dt + @updatePaths() onIdleChanged: (e) -> @setPaused e.idle unless @ended @@ -540,10 +539,12 @@ module.exports = Surface = class Surface extends CocoClass #- Camera focus on hero focusOnHero: -> + hadHero = @heroLank @heroLank = @lankBoss.lankFor 'Hero Placeholder' if me.team is 'ogres' # TODO: do this for real @heroLank = @lankBoss.lankFor 'Hero Placeholder 1' + @updatePaths() if not hadHero #- Real-time playback @@ -576,22 +577,16 @@ module.exports = Surface = class Surface extends CocoClass - #- Paths - TODO: move to LankBoss? but only update on frame drawing instead of on every frame update? - updatePaths: -> - return # TODO: Get paths working again with WebGL - return unless @options.paths - return if @casting + return unless @options.paths and @heroLank + return unless me.isAdmin() # TODO: Fix world thang points, targets, then remove this @hidePaths() - selectedThang = @lankBoss.selectedLank?.thang return if @world.showPaths is 'never' - return if @world.showPaths is 'paused' and @playing - return if @world.showPaths is 'selected' and not selectedThang - @trailmaster ?= new path.Trailmaster @camera - selectedOnly = @playing and @world.showPaths is 'selected' - @paths = @trailmaster.generatePaths @world, @getCurrentFrame(), selectedThang, @lankBoss.lanks, selectedOnly + layerAdapter = @lankBoss.layerAdapters['Path'] + @trailmaster ?= new TrailMaster @camera, layerAdapter + @paths = @trailmaster.generatePaths @world, @heroLank.thang @paths.name = 'paths' - @lankBoss.layerAdapters['Path'].addChild @paths + layerAdapter.addChild @paths hidePaths: -> return if not @paths @@ -699,6 +694,7 @@ module.exports = Surface = class Surface extends CocoClass @normalStage.clear() @webGLStage.clear() @musicPlayer?.destroy() + @trailmaster?.destroy() @normalStage.removeAllChildren() @webGLStage.removeAllChildren() @webGLStage.removeEventListener 'stagemousemove', @onMouseMove diff --git a/app/lib/surface/TrailMaster.coffee b/app/lib/surface/TrailMaster.coffee new file mode 100644 index 000000000..93b22d8ba --- /dev/null +++ b/app/lib/surface/TrailMaster.coffee @@ -0,0 +1,108 @@ +PAST_PATH_ALPHA = 0.75 +PAST_PATH_WIDTH = 5 +FUTURE_PATH_ALPHA = 0.4 +FUTURE_PATH_WIDTH = 2 + +Camera = require './Camera' +CocoClass = require 'lib/CocoClass' + +module.exports = class TrailMaster extends CocoClass + paths: null # dictionary of thang ids to containers for their paths + pathDisplayObject: null + world: null + + constructor: (@camera, @layerAdapter) -> + super() + @tweenedSprites = [] + @listenTo @layerAdapter, 'new-spritesheet', -> @generatePaths(@world, @thang) + + generatePaths: (@world, @thang) -> + return if @generatingPaths + @generatingPaths = true + @cleanUp() + @createGraphics() + @pathDisplayObject = new createjs.SpriteContainer(@layerAdapter.spriteSheet) + @pathDisplayObject.mouseEnabled = @pathDisplayObject.mouseChildren = false + @pathDisplayObject.addChild @createFuturePath() +# @pathDisplayObject.addChild @createPastPath() # Just made the animated path the full path... do we want to have past and future look different again? + @pathDisplayObject.addChild @createTargets() + @generatingPaths = false + return @pathDisplayObject + + cleanUp: -> + createjs.Tween.removeTweens(sprite) for sprite in @tweenedSprites + @tweenedSprites = [] + + createGraphics: -> + color = @colorForThang(@thang.team, PAST_PATH_ALPHA) + @targetDotKey = @cachePathDot(10, color) + @pastDotKey = @cachePathDot(PAST_PATH_WIDTH, color) + @futureDotKey = @cachePathDot(FUTURE_PATH_WIDTH, @colorForThang(@thang.team, FUTURE_PATH_ALPHA)) + + cachePathDot: (width, color) -> + key = "path-dot-#{width}-#{color}" + color = createjs.Graphics.getRGB(color...) + unless key in @layerAdapter.spriteSheet.getAnimations() + circle = new createjs.Shape() + radius = width/2 + circle.graphics.setStrokeStyle(1).beginFill(color).beginStroke('#000000').drawCircle(0, 0, radius) + @layerAdapter.addCustomGraphic(key, circle, [-radius*1.5, -radius*1.5, radius*3, radius*3]) + return key + + colorForThang: (team, alpha=1.0) -> + rgb = [0, 255, 0] + rgb = [255, 0, 0] if team is 'humans' + rgb = [0, 0, 255] if team is 'ogres' + rgb.push(alpha) + return rgb + + createPastPath: -> + return unless points = @world.pointsForThang @thang.id, @camera + params = { interval: 8, frameKey: @pastDotKey } + return @createPath(points, params) + + createFuturePath: -> + return unless points = @world.pointsForThang @thang.id, @camera + interval = Math.max(1, parseInt(@world.frameRate / 4)) + params = { interval: interval, animate: true, frameKey: @futureDotKey } + return @createPath(points, params) + + createTargets: -> + return unless @thang.allTargets + container = new createjs.SpriteContainer(@layerAdapter.spriteSheet) + for x, i in @thang.allTargets by 2 + y = @thang.allTargets[i + 1] + sup = @camera.worldToSurface x: x, y: y + sprite = new createjs.Sprite(@layerAdapter.spriteSheet) + sprite.scaleX = sprite.scaleY = 1 / @layerAdapter.resolutionFactor + sprite.gotoAndStop(@targetDotKey) + sprite.x = sup.x + sprite.y = sup.y + container.addChild(sprite) + return container + + createPath: (points, options={}) -> + options = options or {} + interval = options.interval or 8 + key = options.frameKey or @pastDotKey + container = new createjs.SpriteContainer(@layerAdapter.spriteSheet) + + for x, i in points by interval * 2 + y = points[i + 1] + sprite = new createjs.Sprite(@layerAdapter.spriteSheet) + sprite.scaleX = sprite.scaleY = 1 / @layerAdapter.resolutionFactor + sprite.gotoAndStop(key) + sprite.x = x + sprite.y = y + container.addChild(sprite) + if lastSprite and options.animate + createjs.Tween.get(lastSprite, {loop: true}).to({x:x, y:y}, 1000) + @tweenedSprites.push lastSprite + lastSprite = sprite + + @logged = true + container + + destroy: -> + @cleanUp() + super() diff --git a/app/lib/surface/path.coffee b/app/lib/surface/path.coffee deleted file mode 100644 index 149d4efbc..000000000 --- a/app/lib/surface/path.coffee +++ /dev/null @@ -1,289 +0,0 @@ -# paths before the current state taper out, -# and have a different color than the future -PAST_PATH_TAIL_BRIGHTNESS = 150 -PAST_PATH_TAIL_ALPHA = 0.3 -PAST_PATH_HEAD_BRIGHTNESS = 200 -PAST_PATH_HEAD_ALPHA = 0.75 - -PAST_PATH_HEAD_LENGTH = 50 -PAST_PATH_TAIL_WIDTH = 2 -PAST_PATH_HEAD_WIDTH = 2 -PAST_PATH_MAX_LENGTH = 200 - -# paths in the future are single color dotted lines -FUT_PATH_BRIGHTNESS = 153 -FUT_PATH_ALPHA = 0.8 -FUT_PATH_HEAD_LENGTH = 0 -FUT_PATH_WIDTH = 1 -FUT_PATH_MAX_LENGTH = 2000 - -# selected paths are single color, and larger, more prominent -# most other properties are the same as non-selected -SELECTED_PATH_TAIL_BRIGHTNESS = 146 -SELECTED_PATH_TAIL_ALPHA = 0.5 -SELECTED_PATH_HEAD_BRIGHTNESS = 200 -SELECTED_PATH_HEAD_ALPHA = 1.0 -SELECTED_PAST_PATH_MAX_LENGTH = 2000 - -FUT_SELECTED_PATH_WIDTH = 3 - -# for sprites along the path -CLONE_INTERVAL = 250 # distance between them, ignored for new actions -CLONE_SCALE = 1.0 -CLONE_ALPHA = 0.4 - -# path defaults -PATH_DOT_LENGTH = 3 -PATH_SEGMENT_LENGTH = 15 # should be > PATH_DOT_LENGTH - -Camera = require './Camera' - -module.exports.Trailmaster = class Trailmaster - paths: null # dictionary of thang ids to containers for their paths - selectedPath: null # container of path selected - pathDisplayObject: null - world: null - clock: 0 - - constructor: (@camera) -> - - tick: -> - @clock += 1 - - generatePaths: (@world, @currentFrame, @selectedThang, @sprites, @selectedOnly) -> - @paths = {} - @pathDisplayObject = new createjs.Container() - @pathDisplayObject.mouseEnabled = @pathDisplayObject.mouseChildren = false - for thang in world.thangs - continue unless thang.isSelectable - continue unless thang.isMovable - continue if @selectedOnly and thang isnt @selectedThang - path = @createPathForThang thang - continue if not path - @pathDisplayObject.addChild path - @paths[thang.id] = path - @pathDisplayObject - - createPathForThang: (thang) -> - container = new createjs.Container() - - path = @createPastPathForThang(thang) - container.addChild(path) if path - - path = @createFuturePathForThang(thang) - container.addChild(path) if path - - targets = @createTargetsForThang(thang) - container.addChild(targets) if targets - - if thang is @selectedThang - sprites = @spritesForThang(thang) - for sprite in sprites - container.addChild(sprite) - - container - - createPastPathForThang: (thang) -> - maxLength = if thang is @selectedThang then SELECTED_PAST_PATH_MAX_LENGTH else PAST_PATH_MAX_LENGTH - start = Math.max(@currentFrame - maxLength, 0) - start = 0 if thang isnt @selectedThang - resolution = if thang is @selectedThang then 4 else 12 - return unless points = @world.pointsForThang thang.id, start, @currentFrame, @camera, resolution - params = - tailWidth: PAST_PATH_TAIL_WIDTH - headWidth: PAST_PATH_HEAD_WIDTH - headLength: PAST_PATH_HEAD_LENGTH - if thang is @selectedThang - params['tailColor'] = colorForThang(thang.team, SELECTED_PATH_TAIL_BRIGHTNESS, SELECTED_PATH_TAIL_ALPHA) - params['headColor'] = colorForThang(thang.team, SELECTED_PATH_HEAD_BRIGHTNESS, SELECTED_PATH_HEAD_ALPHA) - else - params['tailColor'] = colorForThang(thang.team, PAST_PATH_TAIL_BRIGHTNESS, PAST_PATH_TAIL_ALPHA) - params['headColor'] = colorForThang(thang.team, PAST_PATH_HEAD_BRIGHTNESS, PAST_PATH_HEAD_ALPHA) - return createPath(points, params) - - - createFuturePathForThang: (thang) -> - resolution = 8 - return unless points = @world.pointsForThang thang.id, @currentFrame, @currentFrame + FUT_PATH_MAX_LENGTH, @camera, resolution - if thang is @selectedThang - color = colorForThang(thang.team, SELECTED_PATH_HEAD_BRIGHTNESS, SELECTED_PATH_HEAD_ALPHA) - else - color = colorForThang(thang.team, FUT_PATH_BRIGHTNESS, FUT_PATH_ALPHA) - return createPath(points, - tailColor: color - tailWidth: if thang is @selectedThang then FUT_SELECTED_PATH_WIDTH else FUT_PATH_WIDTH - headLength: FUT_PATH_HEAD_LENGTH - dotted: true - dotOffset: @clock - ) - - createTargetsForThang: (thang) -> - return unless thang.allTargets - g = new createjs.Graphics() - g.setStrokeStyle(0.5) - g.beginStroke(createjs.Graphics.getRGB(0, 0, 0)) - color = colorForThang(thang.team) - - i = 0 - while i < thang.allTargets.length - g.beginStroke(createjs.Graphics.getRGB(0, 0, 0)) - g.beginFill(createjs.Graphics.getRGB(color...)) - sup = @camera.worldToSurface x: thang.allTargets[i], y: thang.allTargets[i + 1] - g.drawEllipse(sup.x - 5, sup.y - 3, 10, 6) - g.endStroke() - - i += 2 - - s = new createjs.Shape(g) - s.x = 0 - s.y = 0 - s - - spritesForThang: (thang) -> - i = 0 - sprites = [] - sprite = @sprites[thang.id] - return sprites unless sprite?.actions - lastPos = @camera.surfaceToWorld x: sprite.sprite.x, y: sprite.sprite.y - minDistance = Math.pow(CLONE_INTERVAL * Camera.MPP, 2) - actions = @world.actionsForThang(thang.id) - lastAction = null - - for action in actions - continue if action.name in ['idle', 'move'] - frame = @world.frames[action.frame] - frame.restoreStateForThang(thang) - - if lastPos - diff = Math.pow(lastPos.x - thang.pos.x, 2) - diff += Math.pow(lastPos.y - thang.pos.y, 2) - continue if diff < minDistance and action.name is lastAction - - clone = sprite.sprite.clone() - clonePos = @camera.worldToSurface thang.pos - clone.x = clonePos.x - clone.y = clonePos.y - clone.alpha = CLONE_ALPHA - clone.scaleX *= CLONE_SCALE - clone.scaleY *= CLONE_SCALE - if sprite.expandActions # old Sprite - sprite.updateRotation(clone, sprite.data) - animActions = sprite.expandActions(if thang.acts then thang.getActionName() else 'idle') - sprite.applyActionsToSprites(animActions, [clone], true) - animation = clone.spriteSheet.getAnimation(clone.currentAnimation) - clone.currentAnimationFrame = Math.min(@clock % (animation.frames.length * 3), animation.frames.length - 1) - else - continue unless animation = sprite.actions[action.name] - sprite.updateRotation clone - animation = sprite.getActionDirection(animation) ? animation # no idea if this ever works - clone.gotoAndStop animation.name - # TODO: use action-specific framerate here? -# clone.currentAnimationFrame = Math.min(@clock % (animation.frames.length * 3), animation.frames.length - 1) - sprites.push(clone) - lastPos = x: thang.pos.x, y: thang.pos.y - lastAction = action.name - - @world.frames[@currentFrame].restoreStateForThang(thang) - sprites - -createPath = (points, options={}, g=null) -> - options = options or {} - tailColor = options.tailColor ? options.headColor - headColor = options.headColor ? options.tailColor - oneColor = true - oneColor = oneColor and headColor[i] is tailColor[i] for i in [0..4] - maxLength = options.maxLength or 0 - tailWidth = options.tailWidth - headWidth = options.headWidth - oneWidth = headWidth is tailWidth - headLength = options.headLength - dotted = options.dotted - dotOffset = if options.dotOffset? then options.dotOffset else 0 - - points = points.slice(-maxLength * 2) if maxLength isnt 0 - points = points.slice(((points.length / 2 + dotOffset) % PATH_SEGMENT_LENGTH) * 2) if dotOffset - g = new createjs.Graphics() unless g - return new createjs.Shape(g) if not points - - g.setStrokeStyle(tailWidth) - g.beginStroke(createjs.Graphics.getRGB(tailColor...)) - g.moveTo(points[0], points[1]) - - headStart = points.length - headLength - [lastX, lastY] = [points[0], points[1]] - - for x, i in points by 2 - continue if i is 0 - y = points[i + 1] - if i >= headStart and not (oneColor and oneWidth) - diff = (i - headStart) / headLength - style = transition(tailWidth, headWidth, diff) - color = colorTransition(tailColor, headColor, diff) - g.setStrokeStyle(style) - g.beginStroke(createjs.Graphics.getRGB(color...)) - g.moveTo(lastX, lastY) if lastX? - - else if dotted - - if false and i < 2 - # Test: footprints - g.beginFill(createjs.Graphics.getRGB(tailColor...)) - xofs = x - lastX - yofs = y - lastY - theta = Math.atan2(yofs, xofs) - [fdist, fwidth] = [4, 2] - fside = if (i + dotOffset) % 4 is 0 then -1 else 1 - fx = [lastX + fside * fdist * (Math.cos(theta) * xofs - Math.sin(theta) * yofs)] - fy = [lastY + fside * fdist * (Math.sin(theta) * xofs - Math.cos(theta) * yofs)] - g.drawCircle(fx, fy, 2) - - offset = ((i / 2) % PATH_SEGMENT_LENGTH) - if offset >= PATH_DOT_LENGTH - if offset is PATH_DOT_LENGTH - g.endStroke() - lastX = x - lastY = y - continue - - else - if offset is 0 - g.beginStroke(createjs.Graphics.getRGB(tailColor...)) - g.moveTo(lastX, lastY) if lastX? - - g.lineTo(x, y) - lastX = x - lastY = y - - g.endStroke() - - s = new createjs.Shape(g) - return s - -colorTransition = (color1, color2, pct) -> - return color1 if pct <= 0 - return color2 if pct >= 1 - - i = 0 - color = [] - while i < 4 - val = transition(color1[i], color2[i], pct) - val = Math.floor(val) if i isnt 3 - color.push(val) - i += 1 - color - -transition = (num1, num2, pct) -> - return num1 if pct <= 0 - return num2 if pct >= 1 - num1 + (num2 - num1) * pct - -colorForThang = (team, brightness=100, alpha=1.0) => - # multipliers should sum to 3.0 - multipliers = [2.0, 0.5, 0.5] if team is 'humans' - multipliers = [0.5, 0.5, 2.0] if team is 'ogres' - multipliers = [2.0, 0.5, 0.5] if not multipliers - color = _.map(multipliers, (m) -> return parseInt(m * brightness)) - color.push(alpha) - return color - -module.exports.createPath = createPath diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee index 3b932200f..a937ceedc 100644 --- a/app/lib/world/world.coffee +++ b/app/lib/world/world.coffee @@ -551,7 +551,7 @@ module.exports = class World freeMemoryAfterEachSerialization: -> @frames[i] = null for frame, i in @frames when i < @frames.length - 1 - pointsForThang: (thangID, frameStart=0, frameEnd=null, camera=null, resolution=4) -> + pointsForThang: (thangID, camera=null) -> # Optimized @pointsForThangCache ?= {} cacheKey = thangID @@ -570,16 +570,7 @@ module.exports = class World allPoints.reverse() @pointsForThangCache[cacheKey] = allPoints - points = [] - [lastX, lastY] = [null, null] - for frameIndex in [Math.floor(frameStart / resolution) ... Math.ceil(frameEnd / resolution)] - x = allPoints[frameIndex * 2 * resolution] - y = allPoints[frameIndex * 2 * resolution + 1] - continue if x is lastX and y is lastY - lastX = x - lastY = y - points.push x, y - points + return allPoints actionsForThang: (thangID, keepIdle=false) -> # Optimized