<commit_before>CocoClass = require 'lib/CocoClass'
{me} = require 'lib/auth'
Layer = require './Layer'
IndieSprite = require 'lib/surface/IndieSprite'
WizardSprite = require 'lib/surface/WizardSprite'
CocoSprite = require 'lib/surface/CocoSprite'
Mark = require './Mark'
Grid = require 'lib/world/Grid'

module.exports = class SpriteBoss extends CocoClass
  subscriptions:
    'bus:player-joined': 'onPlayerJoined'
    'bus:player-left': 'onPlayerLeft'
    'level-set-debug': 'onSetDebug'
    'level-highlight-sprites': 'onHighlightSprites'
    'surface:stage-mouse-down': 'onStageMouseDown'
    'level-select-sprite': 'onSelectSprite'
    'level-suppress-selection-sounds': 'onSuppressSelectionSounds'
    'level-lock-select': 'onSetLockSelect'
    'level:restarted': 'onLevelRestarted'
    'god:new-world-created': 'onNewWorld'
    'camera:dragged': 'onCameraDragged'
    'sprite:loaded': -> @update(true)

  constructor: (@options) ->
    super()
    @dragged = 0
    @options ?= {}
    @camera = @options.camera
    @surfaceLayer = @options.surfaceLayer
    @surfaceTextLayer = @options.surfaceTextLayer
    @world = options.world
    @options.thangTypes ?= []
    @sprites = {}
    @spriteArray = []  # Mirror @sprites, but faster for when we just need to iterate
    @selfWizardSprite = null
    @createLayers()
    @spriteSheetCache = {}

  destroy: ->
    @removeSprite sprite for thangID, sprite of @sprites
    @targetMark?.destroy()
    @selectionMark?.destroy()
    super()

  toString: -> "<SpriteBoss: #{@spriteArray.length} sprites>"

  thangTypeFor: (type) ->
    _.find @options.thangTypes, (m) -> m.get('original') is type or m.get('name') is type

  createLayers: ->
    @spriteLayers = {}
    for [name, priority] in [
      ['Land', -40]
      ['Ground', -30]
      ['Obstacle', -20]
      ['Path', -10]
      ['Default', 0]
      ['Floating', 10]
    ]
      @spriteLayers[name] = new Layer name: name, layerPriority: priority, transform: Layer.TRANSFORM_CHILD, camera: @camera
    @surfaceLayer.addChild _.values(@spriteLayers)...

  layerForChild: (child, sprite) ->
    unless child.layerPriority?
      # TODO: make better system
      child.layerPriority = 0 if sprite?.thang?.isSelectable
      child.layerPriority = -40 if sprite?.thang?.isLand
    return @spriteLayers['Default'] unless child.layerPriority
    layer = _.findLast @spriteLayers, (layer, name) ->
      layer.layerPriority <= child.layerPriority
    #console.log 'layer for', child, 'is', (layer ? @spriteLayers['Default'])
    layer ? @spriteLayers['Default']

  addSprite: (sprite, id=null, layer=null) ->
    id ?= sprite.thang.id
    console.error 'Sprite collision! Already have:', id if @sprites[id]
    @sprites[id] = sprite
    @spriteArray.push sprite
    layer ?= @spriteLayers['Obstacle'] if sprite.thang?.spriteName.search(/(dungeon|indoor).wall/i) isnt -1
    layer ?= @layerForChild sprite.imageObject, sprite
    layer.addChild sprite.imageObject
    layer.updateLayerOrder()
    sprite

  createMarks: ->
    @targetMark = new Mark name: 'target', camera: @camera, layer: @spriteLayers['Ground'], thangType: 'target'
    @selectionMark = new Mark name: 'selection', camera: @camera, layer: @spriteLayers['Ground'], thangType: 'selection'

  createSpriteOptions: (options) ->
    _.extend options, camera: @camera, resolutionFactor: 4, groundLayer: @spriteLayers['Ground'], textLayer: @surfaceTextLayer, floatingLayer: @spriteLayers['Floating'], spriteSheetCache: @spriteSheetCache, showInvisible: @options.showInvisible

  createIndieSprites: (indieSprites, withWizards) ->
    unless @indieSprites
      @indieSprites = []
      @indieSprites = (@createIndieSprite indieSprite for indieSprite in indieSprites) if indieSprites
    if withWizards and not @selfWizardSprite
      @selfWizardSprite = @createWizardSprite thangID: 'My Wizard', isSelf: true, sprites: @sprites

  createIndieSprite: (indieSprite) ->
    unless thangType = @thangTypeFor indieSprite.thangType
      console.warn "Need to convert #{indieSprite.id}'s ThangType #{indieSprite.thangType} to a ThangType reference. Until then, #{indieSprite.id} won't show up."
      return
    sprite = new IndieSprite thangType, @createSpriteOptions {thangID: indieSprite.id, pos: indieSprite.pos, sprites: @sprites, colorConfig: indieSprite.colorConfig}
    @addSprite sprite, sprite.thang.id

  createOpponentWizard: (opponent) ->
    # TODO: colorize name and cloud by team, colorize wizard by user's color config, level-specific wizard spawn points
    sprite = @createWizardSprite thangID: opponent.id, name: opponent.name, codeLanguage: opponent.codeLanguage
    if not opponent.levelSlug or opponent.levelSlug is 'brawlwood'
      sprite.targetPos = if opponent.team is 'ogres' then {x: 52, y: 52} else {x: 28, y: 28}
    else if opponent.levelSlug is 'dungeon-arena'
      sprite.targetPos = if opponent.team is 'ogres' then {x: 72, y: 39} else {x: 9, y: 39}
    else if opponent.levelSlug is 'criss-cross'
      sprite.targetPos = if opponent.team is 'ogres' then {x: 50, y: 12} else {x: 0, y: 40}
    else
      sprite.targetPos = if opponent.team is 'ogres' then {x: 52, y: 28} else {x: 20, y: 28}

  createWizardSprite: (options) ->
    sprite = new WizardSprite @thangTypeFor('Wizard'), @createSpriteOptions(options)
    @addSprite sprite, sprite.thang.id, @spriteLayers['Floating']

  onPlayerJoined: (e) ->
    # Create another WizardSprite, unless this player is just me
    pid = e.player.id
    return if pid is me.id
    wiz = @createWizardSprite thangID: pid, sprites: @sprites
    wiz.animateIn()
    state = e.player.wizard or {}
    wiz.setInitialState(state.targetPos, @sprites[state.targetSprite])

  onPlayerLeft: (e) ->
    pid = e.player.id
    @sprites[pid]?.animateOut => @removeSprite @sprites[pid]

  onSetDebug: (e) ->
    return if e.debug is @debug
    @debug = e.debug
    sprite.setDebug @debug for sprite in @spriteArray

  onHighlightSprites: (e) ->
    highlightedIDs = e.thangIDs or []
    for thangID, sprite of @sprites
      sprite.setHighlight? thangID in highlightedIDs, e.delay

  addThangToSprites: (thang, layer=null) ->
    return console.warn 'Tried to add Thang to the surface it already has:', thang.id if @sprites[thang.id]
    thangType = _.find @options.thangTypes, (m) ->
      return false unless m.get('actions') or m.get('raster')
      return m.get('name') is thang.spriteName
    thangType ?= _.find @options.thangTypes, (m) -> return m.get('name') is thang.spriteName

    options = @createSpriteOptions thang: thang
    options.resolutionFactor = if thangType.get('kind') is 'Floor' then 2 else SPRITE_RESOLUTION_FACTOR
    sprite = new CocoSprite thangType, options
    @listenTo sprite, 'sprite:mouse-up', @onSpriteMouseUp
    @addSprite sprite, null, layer
    sprite.setDebug @debug
    sprite

  removeSprite: (sprite) ->
    sprite.imageObject.parent.removeChild sprite.imageObject
    thang = sprite.thang
    delete @sprites[sprite.thang.id]
    @spriteArray.splice @spriteArray.indexOf(sprite), 1
    @stopListening sprite
    sprite.destroy()
    sprite.thang = thang  # Keep around so that we know which thang the destroyed thang was for

  updateSounds: ->
    sprite.playSounds() for sprite in @spriteArray  # hmm; doesn't work for sprites which we didn't add yet in adjustSpriteExistence

  update: (frameChanged) ->
    @adjustSpriteExistence() if frameChanged
    sprite.update frameChanged for sprite in @spriteArray
    @updateSelection()
    @spriteLayers['Default'].updateLayerOrder()
    @cache()

  adjustSpriteExistence: ->
    # Add anything new, remove anything old, update everything current
    updateCache = false
    for thang in @world.thangs when thang.exists
      if sprite = @sprites[thang.id]
        sprite.setThang thang  # make sure Sprite has latest Thang
      else
        sprite = @addThangToSprites(thang)
        Backbone.Mediator.publish 'surface:new-thang-added', thang:thang, sprite:sprite
        updateCache = updateCache or sprite.imageObject.parent is @spriteLayers['Obstacle']
        sprite.playSounds()
    for thangID, sprite of @sprites
      missing = not (sprite.notOfThisWorld or @world.thangMap[thangID]?.exists)
      isObstacle = sprite.imageObject.parent is @spriteLayers['Obstacle']
      updateCache = updateCache or (isObstacle and (missing or sprite.hasMoved))
      sprite.hasMoved = false
      @removeSprite sprite if missing
    @cache true if updateCache and @cached

    # mainly for handling selecting thangs from session when the thang is not always in existence
    if @willSelectThang and @sprites[@willSelectThang[0]]
      @selectThang @willSelectThang...

  cache: (update=false) ->
    return if @cached and not update<commit_msg>Made sure items equip themselves within the SpriteBoss, too.<commit_after>CocoClass = require 'lib/CocoClass'
{me} = require 'lib/auth'
Layer = require './Layer'
IndieSprite = require 'lib/surface/IndieSprite'
WizardSprite = require 'lib/surface/WizardSprite'
CocoSprite = require 'lib/surface/CocoSprite'
Mark = require './Mark'
Grid = require 'lib/world/Grid'

module.exports = class SpriteBoss extends CocoClass
  subscriptions:
    'bus:player-joined': 'onPlayerJoined'
    'bus:player-left': 'onPlayerLeft'
    'level-set-debug': 'onSetDebug'
    'level-highlight-sprites': 'onHighlightSprites'
    'surface:stage-mouse-down': 'onStageMouseDown'
    'level-select-sprite': 'onSelectSprite'
    'level-suppress-selection-sounds': 'onSuppressSelectionSounds'
    'level-lock-select': 'onSetLockSelect'
    'level:restarted': 'onLevelRestarted'
    'god:new-world-created': 'onNewWorld'
    'camera:dragged': 'onCameraDragged'
    'sprite:loaded': -> @update(true)

  constructor: (@options) ->
    super()
    @dragged = 0
    @options ?= {}
    @camera = @options.camera
    @surfaceLayer = @options.surfaceLayer
    @surfaceTextLayer = @options.surfaceTextLayer
    @world = options.world
    @options.thangTypes ?= []
    @sprites = {}
    @spriteArray = []  # Mirror @sprites, but faster for when we just need to iterate
    @selfWizardSprite = null
    @createLayers()
    @spriteSheetCache = {}

  destroy: ->
    @removeSprite sprite for thangID, sprite of @sprites
    @targetMark?.destroy()
    @selectionMark?.destroy()
    super()

  toString: -> "<SpriteBoss: #{@spriteArray.length} sprites>"

  thangTypeFor: (type) ->
    _.find @options.thangTypes, (m) -> m.get('original') is type or m.get('name') is type

  createLayers: ->
    @spriteLayers = {}
    for [name, priority] in [
      ['Land', -40]
      ['Ground', -30]
      ['Obstacle', -20]
      ['Path', -10]
      ['Default', 0]
      ['Floating', 10]
    ]
      @spriteLayers[name] = new Layer name: name, layerPriority: priority, transform: Layer.TRANSFORM_CHILD, camera: @camera
    @surfaceLayer.addChild _.values(@spriteLayers)...

  layerForChild: (child, sprite) ->
    unless child.layerPriority?
      # TODO: make better system
      child.layerPriority = 0 if sprite?.thang?.isSelectable
      child.layerPriority = -40 if sprite?.thang?.isLand
    return @spriteLayers['Default'] unless child.layerPriority
    layer = _.findLast @spriteLayers, (layer, name) ->
      layer.layerPriority <= child.layerPriority
    #console.log 'layer for', child, 'is', (layer ? @spriteLayers['Default'])
    layer ? @spriteLayers['Default']

  addSprite: (sprite, id=null, layer=null) ->
    id ?= sprite.thang.id
    console.error 'Sprite collision! Already have:', id if @sprites[id]
    @sprites[id] = sprite
    @spriteArray.push sprite
    layer ?= @spriteLayers['Obstacle'] if sprite.thang?.spriteName.search(/(dungeon|indoor).wall/i) isnt -1
    layer ?= @layerForChild sprite.imageObject, sprite
    layer.addChild sprite.imageObject
    layer.updateLayerOrder()
    sprite

  createMarks: ->
    @targetMark = new Mark name: 'target', camera: @camera, layer: @spriteLayers['Ground'], thangType: 'target'
    @selectionMark = new Mark name: 'selection', camera: @camera, layer: @spriteLayers['Ground'], thangType: 'selection'

  createSpriteOptions: (options) ->
    _.extend options, camera: @camera, resolutionFactor: 4, groundLayer: @spriteLayers['Ground'], textLayer: @surfaceTextLayer, floatingLayer: @spriteLayers['Floating'], spriteSheetCache: @spriteSheetCache, showInvisible: @options.showInvisible

  createIndieSprites: (indieSprites, withWizards) ->
    unless @indieSprites
      @indieSprites = []
      @indieSprites = (@createIndieSprite indieSprite for indieSprite in indieSprites) if indieSprites
    if withWizards and not @selfWizardSprite
      @selfWizardSprite = @createWizardSprite thangID: 'My Wizard', isSelf: true, sprites: @sprites

  createIndieSprite: (indieSprite) ->
    unless thangType = @thangTypeFor indieSprite.thangType
      console.warn "Need to convert #{indieSprite.id}'s ThangType #{indieSprite.thangType} to a ThangType reference. Until then, #{indieSprite.id} won't show up."
      return
    sprite = new IndieSprite thangType, @createSpriteOptions {thangID: indieSprite.id, pos: indieSprite.pos, sprites: @sprites, colorConfig: indieSprite.colorConfig}
    @addSprite sprite, sprite.thang.id

  createOpponentWizard: (opponent) ->
    # TODO: colorize name and cloud by team, colorize wizard by user's color config, level-specific wizard spawn points
    sprite = @createWizardSprite thangID: opponent.id, name: opponent.name, codeLanguage: opponent.codeLanguage
    if not opponent.levelSlug or opponent.levelSlug is 'brawlwood'
      sprite.targetPos = if opponent.team is 'ogres' then {x: 52, y: 52} else {x: 28, y: 28}
    else if opponent.levelSlug is 'dungeon-arena'
      sprite.targetPos = if opponent.team is 'ogres' then {x: 72, y: 39} else {x: 9, y: 39}
    else if opponent.levelSlug is 'criss-cross'
      sprite.targetPos = if opponent.team is 'ogres' then {x: 50, y: 12} else {x: 0, y: 40}
    else
      sprite.targetPos = if opponent.team is 'ogres' then {x: 52, y: 28} else {x: 20, y: 28}

  createWizardSprite: (options) ->
    sprite = new WizardSprite @thangTypeFor('Wizard'), @createSpriteOptions(options)
    @addSprite sprite, sprite.thang.id, @spriteLayers['Floating']

  onPlayerJoined: (e) ->
    # Create another WizardSprite, unless this player is just me
    pid = e.player.id
    return if pid is me.id
    wiz = @createWizardSprite thangID: pid, sprites: @sprites
    wiz.animateIn()
    state = e.player.wizard or {}
    wiz.setInitialState(state.targetPos, @sprites[state.targetSprite])

  onPlayerLeft: (e) ->
    pid = e.player.id
    @sprites[pid]?.animateOut => @removeSprite @sprites[pid]

  onSetDebug: (e) ->
    return if e.debug is @debug
    @debug = e.debug
    sprite.setDebug @debug for sprite in @spriteArray

  onHighlightSprites: (e) ->
    highlightedIDs = e.thangIDs or []
    for thangID, sprite of @sprites
      sprite.setHighlight? thangID in highlightedIDs, e.delay

  addThangToSprites: (thang, layer=null) ->
    return console.warn 'Tried to add Thang to the surface it already has:', thang.id if @sprites[thang.id]
    thangType = _.find @options.thangTypes, (m) ->
      return false unless m.get('actions') or m.get('raster')
      return m.get('name') is thang.spriteName
    thangType ?= _.find @options.thangTypes, (m) -> return m.get('name') is thang.spriteName

    options = @createSpriteOptions thang: thang
    options.resolutionFactor = if thangType.get('kind') is 'Floor' then 2 else SPRITE_RESOLUTION_FACTOR
    sprite = new CocoSprite thangType, options
    @listenTo sprite, 'sprite:mouse-up', @onSpriteMouseUp
    @addSprite sprite, null, layer
    sprite.setDebug @debug
    sprite

  removeSprite: (sprite) ->
    sprite.imageObject.parent.removeChild sprite.imageObject
    thang = sprite.thang
    delete @sprites[sprite.thang.id]
    @spriteArray.splice @spriteArray.indexOf(sprite), 1
    @stopListening sprite
    sprite.destroy()
    sprite.thang = thang  # Keep around so that we know which thang the destroyed thang was for

  updateSounds: ->
    sprite.playSounds() for sprite in @spriteArray  # hmm; doesn't work for sprites which we didn't add yet in adjustSpriteExistence

  update: (frameChanged) ->
    @adjustSpriteExistence() if frameChanged
    sprite.update frameChanged for sprite in @spriteArray
    @updateSelection()
    @spriteLayers['Default'].updateLayerOrder()
    @cache()

  adjustSpriteExistence: ->
    # Add anything new, remove anything old, update everything current
    updateCache = false
    for thang in @world.thangs when thang.exists
      thang.equip() if thang.equip and not thang.equipped  # Pretty hacky, since initialize may not be called
      if sprite = @sprites[thang.id]
        sprite.setThang thang  # make sure Sprite has latest Thang
      else
        sprite = @addThangToSprites(thang)
        Backbone.Mediator.publish 'surface:new-thang-added', thang:thang, sprite:sprite
        updateCache = updateCache or sprite.imageObject.parent is @spriteLayers['Obstacle']
        sprite.playSounds()
    for thangID, sprite of @sprites
      missing = not (sprite.notOfThisWorld or @world.thangMap[thangID]?.exists)
      isObstacle = sprite.imageObject.parent is @spriteLayers['Obstacle']
      updateCache = updateCache or (isObstacle and (missing or sprite.hasMoved))
      sprite.hasMoved = false
      @removeSprite sprite if missing
    @cache true if updateCache and @cached

    # mainly for handling selecting thangs from session when the thang is not always in existence
    if @willSelectThang and @sprites[@willSelectThang[0]]
      @selectThang @willSelectThang...

  cache: (update=false) ->
    return if @cached and not update
{'fraction_non_alphanumeric': 0.11715694609760245, 'fraction_numerical': 0.005667970299835629, 'mean_word_length': 3.8273597811217512, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7370062', 'n_tokens_mistral': 5947, 'n_tokens_neox': 5693, 'n_words': 1505}