primeiro commit
This commit is contained in:
599
internal/vn/game.go
Normal file
599
internal/vn/game.go
Normal file
@@ -0,0 +1,599 @@
|
||||
package vn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/text"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
)
|
||||
|
||||
const (
|
||||
screenWidth = 1280
|
||||
screenHeight = 720
|
||||
)
|
||||
|
||||
type gameMode int
|
||||
|
||||
const (
|
||||
modeTitle gameMode = iota
|
||||
modePlaying
|
||||
modeLoading
|
||||
)
|
||||
|
||||
var colorByName = map[string]color.RGBA{
|
||||
"night": {R: 20, G: 30, B: 60, A: 255},
|
||||
"dawn": {R: 238, G: 178, B: 97, A: 255},
|
||||
"library": {R: 95, G: 62, B: 39, A: 255},
|
||||
"city": {R: 36, G: 36, B: 36, A: 255},
|
||||
"default": {R: 46, G: 86, B: 105, A: 255},
|
||||
}
|
||||
|
||||
// Game is the Ebiten runtime adapter for Engine.
|
||||
type Game struct {
|
||||
engine *Engine
|
||||
script *Script
|
||||
texts *Texts
|
||||
|
||||
scriptBaseDir string
|
||||
savePath string
|
||||
mode gameMode
|
||||
titleSelected int
|
||||
|
||||
notice string
|
||||
noticeFrames int
|
||||
|
||||
imageCache map[string]*ebiten.Image
|
||||
imageErrs map[string]error
|
||||
dialogBox *ebiten.Image
|
||||
|
||||
flightCheckDone chan struct{}
|
||||
flightCheckResult *FlightCheckResult
|
||||
|
||||
debugEnabled bool
|
||||
debugInput string
|
||||
}
|
||||
|
||||
func NewGame(engine *Engine, script *Script, texts *Texts, savePath string) *Game {
|
||||
dialogBox := ebiten.NewImage(screenWidth-80, 220)
|
||||
dialogBox.Fill(color.RGBA{R: 10, G: 10, B: 10, A: 190})
|
||||
|
||||
g := &Game{
|
||||
engine: engine,
|
||||
script: script,
|
||||
texts: texts,
|
||||
scriptBaseDir: script.BaseDir,
|
||||
savePath: savePath,
|
||||
mode: modeLoading,
|
||||
imageCache: map[string]*ebiten.Image{},
|
||||
imageErrs: map[string]error{},
|
||||
dialogBox: dialogBox,
|
||||
flightCheckDone: make(chan struct{}),
|
||||
}
|
||||
go func() {
|
||||
g.flightCheckResult = RunFlightCheck(script, texts)
|
||||
close(g.flightCheckDone)
|
||||
}()
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *Game) Update() error {
|
||||
if g.noticeFrames > 0 {
|
||||
g.noticeFrames--
|
||||
}
|
||||
|
||||
if g.mode == modeLoading {
|
||||
select {
|
||||
case <-g.flightCheckDone:
|
||||
g.mode = modeTitle
|
||||
if g.flightCheckResult != nil && !g.flightCheckResult.OK() {
|
||||
g.setNotice(g.flightCheckResult.Summary(g.texts))
|
||||
} else {
|
||||
g.setNotice(g.texts.FlightCheckPassed)
|
||||
}
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if g.mode == modeTitle {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
|
||||
g.titleSelected = (g.titleSelected + 2) % 3
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
|
||||
g.titleSelected = (g.titleSelected + 1) % 3
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
|
||||
switch g.titleSelected {
|
||||
case 0:
|
||||
g.engine.Reset()
|
||||
g.mode = modePlaying
|
||||
g.setNotice(g.texts.NoticeNewGame)
|
||||
case 1:
|
||||
if err := g.engine.LoadFromFile(g.savePath); err != nil {
|
||||
g.setNotice(fmt.Sprintf(g.texts.NoticeLoadFailed, err.Error()))
|
||||
} else {
|
||||
g.mode = modePlaying
|
||||
g.setNotice(g.texts.NoticeLoaded)
|
||||
}
|
||||
case 2:
|
||||
return ebiten.Termination
|
||||
}
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
return ebiten.Termination
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
st := g.engine.State()
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyF2) {
|
||||
g.debugEnabled = !g.debugEnabled
|
||||
if g.debugEnabled {
|
||||
g.setNotice(g.texts.NoticeDebugOn)
|
||||
} else {
|
||||
g.debugInput = ""
|
||||
g.setNotice(g.texts.NoticeDebugOff)
|
||||
}
|
||||
}
|
||||
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyF5) {
|
||||
if err := g.engine.SaveToFile(g.savePath); err != nil {
|
||||
g.setNotice(fmt.Sprintf(g.texts.NoticeSaveFailed, err.Error()))
|
||||
} else {
|
||||
g.setNotice(g.texts.NoticeSaved)
|
||||
}
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyF9) {
|
||||
if err := g.engine.LoadFromFile(g.savePath); err != nil {
|
||||
g.setNotice(fmt.Sprintf(g.texts.NoticeLoadFailed, err.Error()))
|
||||
} else {
|
||||
g.setNotice(g.texts.NoticeLoaded)
|
||||
}
|
||||
}
|
||||
|
||||
if st.Ended {
|
||||
if g.debugEnabled {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) && strings.TrimSpace(g.debugInput) != "" {
|
||||
target := strings.TrimSpace(g.debugInput)
|
||||
if err := g.engine.JumpToScene(target); err != nil {
|
||||
g.setNotice(fmt.Sprintf(g.texts.NoticeJumpFailed, err.Error()))
|
||||
} else {
|
||||
g.setNotice(fmt.Sprintf(g.texts.NoticeJumped, target))
|
||||
g.debugInput = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
g.captureDebugInput()
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
g.mode = modeTitle
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
g.mode = modeTitle
|
||||
return nil
|
||||
}
|
||||
|
||||
if st.MenuActive {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
|
||||
g.engine.MoveMenu(-1, 0)
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
|
||||
g.engine.MoveMenu(1, 0)
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
|
||||
g.engine.MoveMenu(0, -1)
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
|
||||
g.engine.MoveMenu(0, 1)
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
|
||||
g.engine.ConfirmMenu()
|
||||
}
|
||||
} else {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
|
||||
g.engine.MoveChoice(-1)
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
|
||||
g.engine.MoveChoice(1)
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
|
||||
if g.debugEnabled && strings.TrimSpace(g.debugInput) != "" {
|
||||
target := strings.TrimSpace(g.debugInput)
|
||||
if err := g.engine.JumpToScene(target); err != nil {
|
||||
g.setNotice(fmt.Sprintf(g.texts.NoticeJumpFailed, err.Error()))
|
||||
} else {
|
||||
g.setNotice(fmt.Sprintf(g.texts.NoticeJumped, target))
|
||||
g.debugInput = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(st.Choices) > 0 {
|
||||
g.engine.ConfirmChoice()
|
||||
} else {
|
||||
g.engine.Advance()
|
||||
}
|
||||
}
|
||||
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||
if len(st.Choices) == 0 {
|
||||
g.engine.Advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if g.debugEnabled {
|
||||
g.captureDebugInput()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Game) Draw(screen *ebiten.Image) {
|
||||
if g.mode == modeLoading {
|
||||
g.drawLoading(screen)
|
||||
return
|
||||
}
|
||||
|
||||
if g.mode == modeTitle {
|
||||
g.drawTitle(screen)
|
||||
return
|
||||
}
|
||||
|
||||
st := g.engine.State()
|
||||
g.drawBackground(screen, st)
|
||||
|
||||
face := basicfont.Face7x13
|
||||
|
||||
if st.CharacterSprite != "" {
|
||||
if sprite, err := g.loadImage(st.CharacterSprite); err == nil {
|
||||
drawImageContain(sprite, screen, 0.5, 0.8, 0.55, 0.55)
|
||||
}
|
||||
}
|
||||
|
||||
if st.MenuActive {
|
||||
g.drawMenu(screen, st)
|
||||
}
|
||||
|
||||
if st.Character != "" {
|
||||
text.Draw(screen, fmt.Sprintf(g.texts.CharacterLabelFmt, st.Character, st.Emotion), face, 30, 40, color.White)
|
||||
}
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Translate(40, screenHeight-260)
|
||||
screen.DrawImage(g.dialogBox, op)
|
||||
|
||||
if st.Dialogue.Speaker != "" {
|
||||
text.Draw(screen, st.Dialogue.Speaker, face, 60, screenHeight-225, color.RGBA{R: 255, G: 220, B: 120, A: 255})
|
||||
}
|
||||
for i, line := range wrap(st.Dialogue.Text, 90) {
|
||||
text.Draw(screen, line, face, 60, screenHeight-190+i*20, color.White)
|
||||
}
|
||||
|
||||
if len(st.Choices) > 0 {
|
||||
for i, c := range st.Choices {
|
||||
prefix := " "
|
||||
clr := color.RGBA{R: 210, G: 210, B: 210, A: 255}
|
||||
if i == st.Selected {
|
||||
prefix = "> "
|
||||
clr = color.RGBA{R: 120, G: 255, B: 180, A: 255}
|
||||
}
|
||||
text.Draw(screen, prefix+c.Text, face, 70, screenHeight-120+i*24, clr)
|
||||
}
|
||||
} else {
|
||||
hint := g.texts.HintAdvance
|
||||
if st.Ended {
|
||||
hint = g.texts.HintEnded
|
||||
}
|
||||
text.Draw(screen, hint, face, screenWidth-220, screenHeight-35, color.RGBA{R: 220, G: 220, B: 220, A: 255})
|
||||
}
|
||||
|
||||
text.Draw(screen, g.texts.StoryControls1, face, 20, screenHeight-40, color.RGBA{R: 210, G: 210, B: 210, A: 255})
|
||||
text.Draw(screen, g.texts.StoryControls2, face, 20, screenHeight-20, color.RGBA{R: 220, G: 220, B: 220, A: 255})
|
||||
|
||||
if err := g.engine.Error(); err != nil {
|
||||
text.Draw(screen, fmt.Sprintf(g.texts.ErrorBannerFmt, err), face, 20, 20, color.RGBA{R: 255, G: 80, B: 80, A: 255})
|
||||
}
|
||||
if g.noticeFrames > 0 {
|
||||
text.Draw(screen, g.notice, face, screenWidth-260, 30, color.RGBA{R: 120, G: 255, B: 180, A: 255})
|
||||
}
|
||||
if g.debugEnabled {
|
||||
g.drawDebugOverlay(screen)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) drawMenu(screen *ebiten.Image, st RenderState) {
|
||||
for i, item := range st.MenuItems {
|
||||
img, err := g.loadImage(item.Sprite)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
x, y := menuPositionToPixels(item.X, item.Y)
|
||||
w, h := drawImageCenteredBounded(img, screen, x, y, 260, 260)
|
||||
if i == st.MenuSelected {
|
||||
pad := 8.0
|
||||
ebitenutil.DrawRect(screen, x-w/2-pad, y-h/2-pad, w+pad*2, 2, color.RGBA{R: 120, G: 255, B: 180, A: 255})
|
||||
ebitenutil.DrawRect(screen, x-w/2-pad, y+h/2+pad-2, w+pad*2, 2, color.RGBA{R: 120, G: 255, B: 180, A: 255})
|
||||
ebitenutil.DrawRect(screen, x-w/2-pad, y-h/2-pad, 2, h+pad*2, color.RGBA{R: 120, G: 255, B: 180, A: 255})
|
||||
ebitenutil.DrawRect(screen, x+w/2+pad-2, y-h/2-pad, 2, h+pad*2, color.RGBA{R: 120, G: 255, B: 180, A: 255})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func menuPositionToPixels(x, y float64) (float64, float64) {
|
||||
px := x
|
||||
py := y
|
||||
if x >= 0 && x <= 1 {
|
||||
px = x * screenWidth
|
||||
}
|
||||
if y >= 0 && y <= 1 {
|
||||
py = y * screenHeight
|
||||
}
|
||||
return px, py
|
||||
}
|
||||
|
||||
func drawImageCenteredBounded(src, dst *ebiten.Image, cx, cy, maxW, maxH float64) (drawW, drawH float64) {
|
||||
sw, sh := src.Bounds().Dx(), src.Bounds().Dy()
|
||||
if sw == 0 || sh == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
scale := min(maxW/float64(sw), maxH/float64(sh))
|
||||
drawW = float64(sw) * scale
|
||||
drawH = float64(sh) * scale
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(cx-drawW/2, cy-drawH/2)
|
||||
dst.DrawImage(src, op)
|
||||
return drawW, drawH
|
||||
}
|
||||
|
||||
func (g *Game) Layout(_, _ int) (int, int) {
|
||||
return screenWidth, screenHeight
|
||||
}
|
||||
|
||||
func (g *Game) drawTitle(screen *ebiten.Image) {
|
||||
screen.Fill(color.RGBA{R: 14, G: 20, B: 32, A: 255})
|
||||
face := basicfont.Face7x13
|
||||
st := g.engine.State()
|
||||
|
||||
text.Draw(screen, st.Title, face, screenWidth/2-70, 180, color.RGBA{R: 240, G: 240, B: 255, A: 255})
|
||||
text.Draw(screen, g.texts.TitleSubtitle, face, screenWidth/2-80, 210, color.RGBA{R: 170, G: 180, B: 210, A: 255})
|
||||
|
||||
items := []string{g.texts.TitleMenuStart, g.texts.TitleMenuLoad, g.texts.TitleMenuQuit}
|
||||
for i, item := range items {
|
||||
prefix := " "
|
||||
clr := color.RGBA{R: 200, G: 200, B: 200, A: 255}
|
||||
if i == g.titleSelected {
|
||||
prefix = "> "
|
||||
clr = color.RGBA{R: 120, G: 255, B: 180, A: 255}
|
||||
}
|
||||
text.Draw(screen, prefix+item, face, screenWidth/2-40, 300+i*36, clr)
|
||||
}
|
||||
|
||||
text.Draw(screen, g.texts.TitleControls, face, screenWidth/2-50, 460, color.RGBA{R: 160, G: 170, B: 190, A: 255})
|
||||
text.Draw(screen, g.texts.TitleControls2, face, screenWidth/2-110, 490, color.RGBA{R: 160, G: 170, B: 190, A: 255})
|
||||
if g.noticeFrames > 0 {
|
||||
text.Draw(screen, g.notice, face, screenWidth/2-120, 520, color.RGBA{R: 255, G: 180, B: 120, A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) drawLoading(screen *ebiten.Image) {
|
||||
screen.Fill(color.RGBA{R: 12, G: 16, B: 24, A: 255})
|
||||
face := basicfont.Face7x13
|
||||
text.Draw(screen, g.script.Title, face, screenWidth/2-70, 220, color.RGBA{R: 230, G: 230, B: 245, A: 255})
|
||||
text.Draw(screen, g.texts.LoadingTitle, face, screenWidth/2-30, 300, color.RGBA{R: 140, G: 240, B: 180, A: 255})
|
||||
text.Draw(screen, g.texts.LoadingMessage, face, screenWidth/2-130, 330, color.RGBA{R: 170, G: 180, B: 200, A: 255})
|
||||
text.Draw(screen, g.texts.LoadingWait, face, screenWidth/2-40, 360, color.RGBA{R: 170, G: 180, B: 200, A: 255})
|
||||
}
|
||||
|
||||
func (g *Game) captureDebugInput() {
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(g.debugInput) > 0 {
|
||||
g.debugInput = g.debugInput[:len(g.debugInput)-1]
|
||||
}
|
||||
keys := inpututil.AppendJustPressedKeys(nil)
|
||||
for _, k := range keys {
|
||||
if ch, ok := keyToSceneChar(k); ok {
|
||||
g.debugInput += ch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func keyToSceneChar(k ebiten.Key) (string, bool) {
|
||||
switch {
|
||||
case k >= ebiten.KeyA && k <= ebiten.KeyZ:
|
||||
return strings.ToLower(string(rune('A' + (k - ebiten.KeyA)))), true
|
||||
case k >= ebiten.Key0 && k <= ebiten.Key9:
|
||||
return string(rune('0' + (k - ebiten.Key0))), true
|
||||
case k == ebiten.KeyMinus:
|
||||
if ebiten.IsKeyPressed(ebiten.KeyShiftLeft) || ebiten.IsKeyPressed(ebiten.KeyShiftRight) {
|
||||
return "_", true
|
||||
}
|
||||
return "-", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (g *Game) drawDebugOverlay(screen *ebiten.Image) {
|
||||
face := basicfont.Face7x13
|
||||
ebitenutil.DrawRect(screen, 20, 60, 420, 600, color.RGBA{R: 0, G: 0, B: 0, A: 180})
|
||||
text.Draw(screen, g.texts.DebugHeader, face, 32, 84, color.RGBA{R: 120, G: 255, B: 180, A: 255})
|
||||
text.Draw(screen, fmt.Sprintf(g.texts.DebugCurrentScene, g.engine.CurrentSceneID()), face, 32, 106, color.White)
|
||||
text.Draw(screen, g.texts.DebugJumpPrefix+g.debugInput, face, 32, 128, color.White)
|
||||
text.Draw(screen, g.texts.DebugJumpHelp, face, 32, 150, color.RGBA{R: 200, G: 200, B: 200, A: 255})
|
||||
|
||||
vars := g.engine.VarsSnapshot()
|
||||
names := make([]string, 0, len(vars))
|
||||
for k := range vars {
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
text.Draw(screen, g.texts.DebugVariables, face, 32, 178, color.RGBA{R: 255, G: 220, B: 120, A: 255})
|
||||
y := 198
|
||||
maxLines := 14
|
||||
for i, name := range names {
|
||||
if i >= maxLines {
|
||||
text.Draw(screen, fmt.Sprintf(g.texts.DebugMoreFmt, len(names)-maxLines), face, 32, y, color.RGBA{R: 180, G: 180, B: 180, A: 255})
|
||||
break
|
||||
}
|
||||
v := vars[name]
|
||||
text.Draw(screen, fmt.Sprintf(g.texts.DebugVarFmt, name, v.Type, v.String()), face, 32, y, color.White)
|
||||
y += 18
|
||||
}
|
||||
|
||||
ids := g.engine.SceneIDs()
|
||||
prefix := strings.TrimSpace(g.debugInput)
|
||||
filtered := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if prefix == "" || strings.HasPrefix(id, prefix) {
|
||||
filtered = append(filtered, id)
|
||||
}
|
||||
}
|
||||
text.Draw(screen, g.texts.DebugScenes, face, 32, 480, color.RGBA{R: 255, G: 220, B: 120, A: 255})
|
||||
y = 500
|
||||
for i, id := range filtered {
|
||||
if i >= 8 {
|
||||
text.Draw(screen, fmt.Sprintf(g.texts.DebugMoreFmt, len(filtered)-8), face, 32, y, color.RGBA{R: 180, G: 180, B: 180, A: 255})
|
||||
break
|
||||
}
|
||||
text.Draw(screen, id, face, 32, y, color.White)
|
||||
y += 18
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) drawBackground(screen *ebiten.Image, st RenderState) {
|
||||
if st.BackgroundImage != "" {
|
||||
if bg, err := g.loadImage(st.BackgroundImage); err == nil {
|
||||
drawImageCover(bg, screen)
|
||||
return
|
||||
}
|
||||
}
|
||||
if looksLikeImagePath(st.Background) {
|
||||
if bg, err := g.loadImage(st.Background); err == nil {
|
||||
drawImageCover(bg, screen)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bg := colorByName["default"]
|
||||
if c, ok := colorByName[strings.ToLower(st.Background)]; ok {
|
||||
bg = c
|
||||
}
|
||||
screen.Fill(bg)
|
||||
}
|
||||
|
||||
func (g *Game) loadImage(path string) (*ebiten.Image, error) {
|
||||
resolved := path
|
||||
if !filepath.IsAbs(path) {
|
||||
resolved = filepath.Join(g.scriptBaseDir, path)
|
||||
}
|
||||
if img, ok := g.imageCache[resolved]; ok {
|
||||
return img, nil
|
||||
}
|
||||
if err, ok := g.imageErrs[resolved]; ok {
|
||||
return nil, err
|
||||
}
|
||||
img, _, err := ebitenutil.NewImageFromFile(resolved)
|
||||
if err != nil {
|
||||
g.imageErrs[resolved] = err
|
||||
return nil, err
|
||||
}
|
||||
g.imageCache[resolved] = img
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func (g *Game) setNotice(msg string) {
|
||||
g.notice = msg
|
||||
g.noticeFrames = 240
|
||||
}
|
||||
|
||||
func looksLikeImagePath(v string) bool {
|
||||
v = strings.ToLower(v)
|
||||
return strings.HasSuffix(v, ".png") || strings.HasSuffix(v, ".jpg") || strings.HasSuffix(v, ".jpeg") || strings.HasSuffix(v, ".gif")
|
||||
}
|
||||
|
||||
func drawImageCover(src, dst *ebiten.Image) {
|
||||
sw, sh := src.Bounds().Dx(), src.Bounds().Dy()
|
||||
dw, dh := dst.Bounds().Dx(), dst.Bounds().Dy()
|
||||
if sw == 0 || sh == 0 {
|
||||
return
|
||||
}
|
||||
scale := max(float64(dw)/float64(sw), float64(dh)/float64(sh))
|
||||
w := float64(sw) * scale
|
||||
h := float64(sh) * scale
|
||||
x := (float64(dw) - w) / 2
|
||||
y := (float64(dh) - h) / 2
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(x, y)
|
||||
dst.DrawImage(src, op)
|
||||
}
|
||||
|
||||
func drawImageContain(src, dst *ebiten.Image, xRatio, yRatio, maxW, maxH float64) {
|
||||
sw, sh := src.Bounds().Dx(), src.Bounds().Dy()
|
||||
dw, dh := dst.Bounds().Dx(), dst.Bounds().Dy()
|
||||
if sw == 0 || sh == 0 {
|
||||
return
|
||||
}
|
||||
maxWpx := float64(dw) * maxW
|
||||
maxHpx := float64(dh) * maxH
|
||||
scale := min(maxWpx/float64(sw), maxHpx/float64(sh))
|
||||
w := float64(sw) * scale
|
||||
h := float64(sh) * scale
|
||||
x := float64(dw)*xRatio - w/2
|
||||
y := float64(dh)*yRatio - h/2
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(x, y)
|
||||
dst.DrawImage(src, op)
|
||||
}
|
||||
|
||||
func max(a, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func min(a, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func wrap(s string, max int) []string {
|
||||
if len(s) <= max {
|
||||
return []string{s}
|
||||
}
|
||||
words := strings.Fields(s)
|
||||
if len(words) == 0 {
|
||||
return []string{""}
|
||||
}
|
||||
var lines []string
|
||||
line := words[0]
|
||||
for _, w := range words[1:] {
|
||||
candidate := line + " " + w
|
||||
if len(candidate) > max {
|
||||
lines = append(lines, line)
|
||||
line = w
|
||||
} else {
|
||||
line = candidate
|
||||
}
|
||||
}
|
||||
lines = append(lines, line)
|
||||
return lines
|
||||
}
|
||||
Reference in New Issue
Block a user