気を散らすノート

色々と散った気をまとめておくところ.論文読んだり自分で遊んだりする.たぶん.

killersheep つづき

つづき…ですが,ある程度読んだところで概ね活躍してる関数には馴染んでしまった気がして, これ以上伸ばしてもあんまりおもしろみがないなと思ったので途中で唐突に終わります. 唐突に終わることも合って advent calendar 外,たんに「つづき」ということにします

func s:IntroFilter(id, key)
  if a:key == 's' || a:key == 'S'
    call s:Clear()
    let s:round = 0
    let s:ready = popup_create('Get Ready!', #{border: [], padding: [2, 4, 2, 4]})
    call s:BlinkLevel(s:ready, 1)
    call timer_start(s:blink_time * 8, { -> s:NextRound()})
    let s:ready_timer = timer_start(300, {... -> s:ReadySound()})
  elseif a:key == 'x' || a:key == 'X' || a:key == "\<Esc>"
    call s:Clear()
  endif
  return 1
endfunc

func s:BlinkLevel(winid, on)
  call popup_setoptions(a:winid, #{highlight: a:on ? 'KillerLevelX': 'KillerLevel'})
  let s:blink_timer = timer_start(s:blink_time, {x -> s:BlinkLevel(a:winid, !a:on)})
endfunc

func s:ReadySound()
  call s:PlaySound('quack')
  let s:ready_timer = timer_start(s:blink_time * 2, {... -> s:ReadySound()})
endfunc

これが popup_createfilter として呼ばれているので,key でどのキーが押下されたかが 受け取れます. s でスタート,x<Esc> で終了,ほかは無視,ですね.

まず timer_start({time}, {callback} [, {options}]) についておさらい

  • 返り値: timer ID
  • time: ms 単位で待ち時間を設定
  • callback: コールバックを設定.timer ID が引数として渡される
  • repeat: 何回 callback を呼ぶか. -1 で無限に呼び続ける

というわけでチカチカ (s:BlinkLevel()) したあとに クエックエックエックエッとs:ReadySound()が繰り返し呼ばれたころに s:NextRound() が呼ばれるようになっている.

func s:NextRound()
  call s:Clear()

  let s:round += 1
  let s:sheepcount = 0
  let s:frozen = 0
  call s:ShowBulletSoon()

  " Every once in a while let the next sheep that moves poop.
  let s:wantpoop = 0
  let s:poop_timer = timer_start(s:poop_interval[s:round - 1], {x -> s:WantPoop()}, #{repeat: -1})

  " Create a few sheep to kill.
  let topline = &lines > 50 ? &lines - 50 : 0
  call s:PlaceSheep(topline +  0,  5, 'KillerSheep')
  call s:PlaceSheep(topline +  5, 75, 'KillerSheep2')
  " 中略
  call s:ShowLevel(topline)

  let s:canon_id = popup_create(['  /#\  ', ' /###\ ', '/#####\'], #{
    \ line: &lines - 2,                 " 開始場所でしたね
    \ highlight: 'KillerCannon',        " ハイライト
    \ filter: function('s:MoveCanon'),  " 操作を受け取る
    \ zindex: s:cannon_zindex,          " CSS と同じ,デフォルト50.
    \ mask: [[1,2,1,1], [6,7,1,1], [1,1,2,2], [7,7,2,2]],
    \                                   " 列行で指定する長方形を透明に
    \ mapping: 0,
    \ })
endfunc

const s:poop_interval = [700, 500, 300, 200, 100] " えげつなくなっていく

こういう画面をイメージしてくれ

f:id:lesguillemets:20191214172602p:plain
機体の様子 顔で何かを思い出しそうになる

まず s:ShowBulletSoon() まわり

func s:ShowBulletSoon()
  let s:bullet_available = 0
  let s:bullet_timer = timer_start(s:bullet_holdoff, {x -> ShowBullet()})
endfunc

func ShowBullet()
  if s:frozen
    return
  endif
  let s:bullet_timer = 0
  let pos = popup_getpos(s:canon_id)
  let s:bullet_available = popup_create(['|', '|'], #{
   \ col: pos.col + 3,
   \ line: &lines - 3,
   \ highlight: 'KillerBullet',
   \ zindex: s:bullet_zindex,
   \ })
endfunc

Bullet は 機体に生えてくる→発射→消滅→機体に生える というのを繰り返してて,ここはその機体に生えるパートだ. popup_getpos({id})id のポップアップの場所を教えてくれるので,機体の場所を取得して そこにKillerBullet 色の | を生やしている.

続いて

let s:poop_timer = timer_start(s:poop_interval[s:round - 1], {x -> s:WantPoop()}, #{repeat: -1})

s:poop_interval ごとに無限に繰り返される s:WantPoop(): let s:wantpoop=1 で状態管理.

それから羊を置く

func s:NextRound()
" ...
  " Create a few sheep to kill.
  let topline = &lines > 50 ? &lines - 50 : 0
  call s:PlaceSheep(topline +  0,  5, 'KillerSheep')
  call s:PlaceSheep(topline +  5, 75, 'KillerSheep2')
  " 中略
  call s:ShowLevel(topline)
"...
endfunction


func s:PlaceSheep(line, col, hl)
  let id = popup_create(s:sheepSprite[0], #{
    \ line: a:line,
    \ col: a:col,
    \ highlight: a:hl,
    \ mask: s:sheepMasks[0],
    \ fixed: 1,
    \ zindex: s:sheep_zindex,
    \ wrap: 0,
    \})
  call setwinvar(id, 'left', 0)
  call setwinvar(id, 'dead', 0)
  call timer_start(s:sheep_anim, {x -> s:AnimSheep(id, 1)})
  let s:sheepcount += 1
  sleep 10m
  return id
endfunc

だいぶ popup_create に慣れてきた.popup_create は window を作るので, setwinvar で変数を覚えさせておくことができるのだった.そうなると s:AnimSheepが… ちょっと長いので諦めてコメントで読んだことにするね

func s:AnimSheep(id, state)
  " 前略
  let pos = popup_getpos(a:id)
  if getwinvar(a:id, 'dead')
    return
  endif
  let left = getwinvar(a:id, 'left')
  if left == 1
    " 左端に来たら
    " i) 高さを判定(だんだん下に降りてきて,下端でゲームオーバ)
    if pos.line > &lines - 11
      call s:PlaySoundForEnd()
    endif
    " ii) 5行降りて右端に戻る
    call popup_setoptions(a:id, #{pos: 'topleft', col: &columns + 1, line: pos.line + 5})
    let left = 0
  elseif pos.col > 1
    " 左に移動
    call popup_move(a:id, #{col: pos.col - 1})
  else " left != 1 && pos.col <= 0
    " 普段は左端から col を数えてる.羊の頭が左端にはみ出られるように
    " 端にきたら `topright` から数えるようにして左に進めていく
    if left == 0
      let left = 15 " ひつじの幅
    endif
    call popup_setoptions(a:id, #{pos: 'topright', col: left - 1})
    let left -= 1
  endif

  " 概ねそのまま
  let poopid = getwinvar(a:id, 'poopid')
  if poopid
    let poopcount = getwinvar(a:id, 'poopcount')
    if poopcount == 1
      " drop the poop
      call popup_close(poopid)
      call setwinvar(a:id, 'poopid', 0)
      call s:Poop(pos.line + 1, left ? left : pos.col + 12)
    else
      call popup_move(poopid, #{col: left ? left + 1 : pos.col + 14,
       \ line: pos.line + 1})
    endif
    call setwinvar(a:id, 'poopcount', poopcount - 1)
  endif

  call setwinvar(a:id, 'left', left)
  call popup_settext(a:id, s:sheepSprite[a:state])
  call popup_setoptions(a:id, #{mask: s:sheepMasks[a:state]})
  call timer_start(s:sheep_anim, {x -> s:AnimSheep(a:id, a:state == 3 ? 0 : a:state + 1)})

  if left || pos.col < &columns - 14
    if s:wantpoop && !getwinvar(a:id, 'poopid')
      let s:wantpoop = 0
      call setwinvar(a:id, 'poopcount', s:poop_countdown)
      let poopid = popup_create('x', #{
        \ col: left ? left + 1 : pos.col + 14,
        \ line: pos.line + 1,
        \ highlight: 'KillerPoop',
        \ zindex: s:bullet_zindex,
        \ })
      call setwinvar(a:id, 'poopid', poopid)
    endif
  endif
endfunc

…ということで i) はじめの描画と ii) ひつじの動き がわかったわけで,概ね読んで楽しいフェーズが終わってきた気がします. あとは弾の当たり判定だけ読んで終わりにします……

func s:MoveBullet(x, id)
  if s:frozen
    return
  endif
  let pos = popup_getpos(a:id)
  if pos == {}
    call timer_stop(a:x)
    return
  endif
  if pos.line <= 2
    call popup_close(a:id)
    call timer_stop(a:x)
  else
    call popup_move(a:id, #{line: pos.line - 2})
    let winid = popup_locate(pos.line - 2, pos.col)
    if winid != 0 && winid != a:id
      call s:KillSheep(winid, 4)
      if s:sheepcount == 1
        let s:frozen = 1
      endif
      call s:PlaySound('beh')
      call popup_close(a:id)
    endif
  endif
endfunc

この popup_locate, ゲーム作るためにあるんじゃないかという関数ですが(?)

popup_locate({row}, {col})               *popup_locate()*
    Return the |window-ID| of the popup at screen position {row}
    and {col}.  If there are multiple popups the one with the
    highest zindex is returned.  If there are no popups at this
    position then zero is returned.

というわけでスッキリ.おしまい(唐突).