Skip to content

Commit

Permalink
Merge pull request #32 from pankona/tower-impl
Browse files Browse the repository at this point in the history
tower impl
  • Loading branch information
pankona authored Jun 25, 2024
2 parents 62d7741 + 2a724c7 commit 681a5e2
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 1 deletion.
4 changes: 4 additions & 0 deletions barricade.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func newBarricade(game *Game, x, y int, onDestroy func(b *barricade)) *barricade
return h
}

func (b *barricade) Update() {
// 何もしない
}

// 画面中央に配置
func (b *barricade) Draw(screen *ebiten.Image) {
// 画像を描画
Expand Down
1 change: 1 addition & 0 deletions building.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Building interface {

Clickable
Drawable
Updater
}

func (g *Game) AddBuilding(b Building) {
Expand Down
2 changes: 2 additions & 0 deletions buildpane.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ func newBuildPane(game *Game) *buildPane {
// クリックされたら建築を確定する
game.AddBuilding(game.buildCandidate)
game.clickHandler.Add(game.buildCandidate)
game.updateHandler.Add(game.buildCandidate)

// クレジットを減らす
game.credit -= game.buildCandidate.Cost()

Expand Down
54 changes: 53 additions & 1 deletion house.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ func newHouse(game *Game) *house {
return h
}

func (h *house) Update() {
// 何もしない
}

// 画面中央に配置
func (h *house) Draw(screen *ebiten.Image) {
// 画像を描画
Expand Down Expand Up @@ -154,7 +158,7 @@ func (h *house) OnClick(x, y int) bool {
ebitenutil.DebugPrintAt(screen, fmt.Sprintf("BUILD ($%d)", CostBarricadeBuild), x+width/2-30, y+height/2+40)

// 選択中であればボタンをハイライト表示する
if h.game.buildCandidate != nil {
if h.game.buildCandidate != nil && h.game.buildCandidate.Name() == "Barricade" {
drawYellowRect(screen, x, y, width, height)
}

Expand All @@ -169,6 +173,54 @@ func (h *house) OnClick(x, y int) bool {
})

h.game.infoPanel.AddButton(buildBarricadeButton)

buildTowerButton := newButton(h.game,
225+infoPanelHeight, eScreenHeight, infoPanelHeight, infoPanelHeight, 1,
func(x, y int) bool {
if h.game.credit < CostTowerBuild {
// お金が足りない場合は建築できない
return false
}

// buildCandidate を持っているときにバリケードボタンを押したときの振る舞い
// 選択肢なおしということ、いったん手放す
if h.game.buildCandidate != nil {
h.game.drawHandler.Remove(h.game.buildCandidate)
}

towerOnDestroyFn := func(b *tower) {
b.game.drawHandler.Remove(b)
b.game.clickHandler.Remove(b)
b.game.RemoveBuilding(b)
b.game.infoPanel.Remove(b)
}

h.game.buildCandidate = newTower(h.game, 0, 0, towerOnDestroyFn)
return false
},
func(screen *ebiten.Image, x, y, width, height int) {
drawRect(screen, x, y, width, height)
towerIcon := newTowerIcon(x+width/2, y+height/2-10)
towerIcon.Draw(screen)

ebitenutil.DebugPrintAt(screen, fmt.Sprintf("BUILD ($%d)", CostTowerBuild), x+width/2-30, y+height/2+40)

// 選択中であればボタンをハイライト表示する
if h.game.buildCandidate != nil && h.game.buildCandidate.Name() == "Tower" {
drawYellowRect(screen, x, y, width, height)
}

// お金が足りないときはボタン全体をグレーアウトする
if h.game.credit < CostTowerBuild {
overlay := ebiten.NewImage(width, height)
overlay.Fill(color.RGBA{128, 128, 128, 128})
overlayOpts := &ebiten.DrawImageOptions{}
overlayOpts.GeoM.Translate(float64(x), float64(y))
screen.DrawImage(overlay, overlayOpts)
}
})
h.game.infoPanel.AddButton(buildTowerButton)

case PhaseWave:
// TODO: implement
default:
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const (
// コスト一覧
const (
CostBarricadeBuild = 30
CostTowerBuild = 50
)

const (
Expand Down
261 changes: 261 additions & 0 deletions tower.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package main

import (
"bytes"
"image"
"log"
"math"

"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"

_ "embed"
"image/color"
_ "image/png"
)

//go:embed assets/tower.png
var towerImageData []byte

type tower struct {
game *Game

x, y int
width, height int
zindex int
image *ebiten.Image

health int
attackRange float64
attackPower int
cooldown int
erapsedTime int // 攻撃実行からの経過時間

// 画像の拡大率。
// 1以外を指定する場合は元画像のサイズをそもそも変更できないか検討すること
scale float64

// health が 0 になったときに呼ばれる関数
onDestroy func(b *tower)

// この建物が他の建物と重なっているかどうか (建築確定前に用いるフラグ)
isOverlapping bool
}

const towerAttackCoolDown = 15

func newTower(game *Game, x, y int, onDestroy func(b *tower)) *tower {
img, _, err := image.Decode(bytes.NewReader(towerImageData))
if err != nil {
log.Fatal(err)
}

h := &tower{
game: game,

x: x,
y: y,
width: img.Bounds().Dx(),
height: img.Bounds().Dy(),
scale: 1,

health: 100,
attackRange: 300,
attackPower: 2,

image: ebiten.NewImageFromImage(img),

onDestroy: onDestroy,
}

return h
}

func (t *tower) Update() {
if t.game.phase == PhaseBuilding {
// do nothing
return
}

// 敵が攻撃範囲に入ってきたら攻撃する
// 複数の敵が攻撃範囲に入ってきた場合は、最も近い敵を攻撃する

// 最寄りの敵を探す
var nearestEnemy Enemy
nearestDistance := math.MaxFloat64
for _, e := range t.game.enemies {
ex, ey := e.Position()
distance := math.Sqrt(math.Pow(float64(t.x-ex), 2) + math.Pow(float64(t.y-ey), 2))
if distance < nearestDistance {
nearestEnemy = e
nearestDistance = distance
}
}

// クールダウンが明けていて、かつ攻撃範囲に入っていれば攻撃する
if t.cooldown == 0 && nearestEnemy != nil && nearestDistance < t.attackRange {
b := nearestEnemy.(*bug)
b.Damage(t.attackPower)
t.cooldown = towerAttackCoolDown

// ビームを描画する
bm := newBeam(t.game, t.x, t.y, b.x, b.y)
t.game.drawHandler.Add(bm)
}

if t.cooldown > 0 {
t.cooldown--
}
}

// タワーから発射されるビームを描画するための構造体
type beam struct {
game *Game

startX, startY int
endX, endY int
width int

// 何フレーム後に消えるか
displayTime int
}

func newBeam(game *Game, startX, startY, endX, endY int) *beam {
return &beam{
game: game,
startX: startX,
startY: startY,
endX: endX,
endY: endY,
width: 7,
displayTime: 10,
}
}

func (b *beam) Draw(screen *ebiten.Image) {
if b.displayTime >= 0 {
vector.StrokeLine(screen, float32(b.startX), float32(b.startY), float32(b.endX), float32(b.endY), float32(b.width), color.RGBA{255, 255, 0, 128}, true)
b.displayTime--
}

if b.displayTime <= 0 {
b.game.drawHandler.Remove(b)
}
}

func (b *beam) ZIndex() int {
return 110
}

// 画面中央に配置
func (b *tower) Draw(screen *ebiten.Image) {
// 画像を描画
opts := &ebiten.DrawImageOptions{}
opts.GeoM.Scale(b.scale, b.scale)
opts.GeoM.Translate(float64(b.x)-float64(b.width)*b.scale/2, float64(b.y)-float64(b.height)*b.scale/2)

// 他の建物と重なっている場合は赤くする
if b.isOverlapping {
opts.ColorScale.Scale(1, 0, 0, 1)
} else if b.game.buildCandidate == b {
// 建築確定前は暗い色で建物を描画する
opts.ColorScale.Scale(0.5, 0.5, 0.5, 1)
}

screen.DrawImage(b.image, opts)
}

func (b *tower) ZIndex() int {
return b.zindex
}

func (b *tower) Position() (int, int) {
return b.x, b.y
}

func (b *tower) SetPosition(x, y int) {
b.x = x
b.y = y
}

func (b *tower) Size() (int, int) {
return int(float64(b.width) * b.scale), int(float64(b.height) * b.scale)
}

func (b *tower) Name() string {
return "Tower"
}

func (b *tower) Damage(d int) {
if b.health <= 0 {
return
}

b.health -= d
if b.health <= 0 {
b.health = 0
b.onDestroy(b)
}
}

// tower implements Clickable interface
func (b *tower) OnClick(x, y int) bool {
b.game.clickedObject = "tower"

// infoPanel に情報を表示する

// TODO: ClearButtons は呼び出し側でやるんじゃなくて infoPanel 側のどっかでやるべきかな
b.game.infoPanel.ClearButtons()
icon := newTowerIcon(80, eScreenHeight+70)
b.game.infoPanel.setIcon(icon)
b.game.infoPanel.setUnit(b)

return false
}

func newTowerIcon(x, y int) *icon {
img, _, err := image.Decode(bytes.NewReader(towerImageData))
if err != nil {
log.Fatal(err)
}

return newIcon(x, y, ebiten.NewImageFromImage(img))
}

func (b *tower) Health() int {
return b.health
}

func (b *tower) IsClicked(x, y int) bool {
w, h := b.Size()
return b.x-w/2 <= x && x <= b.x+w/2 && b.y-h/2 <= y && y <= b.y+h/2
}

func (b *tower) SetOverlap(overlap bool) {
b.isOverlapping = overlap
}

func (b *tower) IsOverlap() bool {
// 他の建物と重なっているかどうかを判定する
for _, building := range b.game.buildings {
if building == b {
continue
}

bx, by := building.Position()
bw, bh := building.Size()

if intersects(
rect{b.x - b.width/2, b.y - b.height/2, b.width, b.height},
rect{bx - bw/2, by - bh/2, bw, bh},
) {
return true
}
}

return false
}

func (b *tower) Cost() int {
return CostTowerBuild
}
6 changes: 6 additions & 0 deletions wavectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ func (w *waveController) Update() {
w.game.clickHandler.Add(gover)
w.game.drawHandler.Add(gover)

// 建物の Update() が呼ばれないようにする
// ゲームオーバー画面で攻撃を続けないようにするため
for _, b := range w.game.buildings {
w.game.updateHandler.Remove(b)
}

// ウェーブ終了みたいなものなので自分を削除する
w.game.updateHandler.Remove(w)
} else if len(w.game.enemies) == 0 {
Expand Down

0 comments on commit 681a5e2

Please sign in to comment.