Skip to content

Commit

Permalink
Merge pull request #1 from braheezy/menu
Browse files Browse the repository at this point in the history
Implement settings menu
  • Loading branch information
braheezy authored Aug 11, 2024
2 parents 2d87c35 + eb1cd7d commit 0555468
Show file tree
Hide file tree
Showing 20 changed files with 698 additions and 46 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
bin/
/sounds/
/invaders
settings.json
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ An emulation of [Space Invaders (1978)](https://www.wikiwand.com/en/Space_Invade

![demo](./demo.webp)

![settings](./settings.png)

This project contains an 8080 emulator and the necessary fake hardware bits to run the original Space Invaders arcade ROM.

## Installation
Expand Down Expand Up @@ -46,5 +48,6 @@ You need Go and the dependencies that [Ebiten engine](https://ebitengine.org/en/
Run `make` for various commands to run.

## Roadmap
- [ ] DIP settings
- [ ] Persistent high score
- [x] DIP settings
- ~~Persistent high score~~~ not realistically possible because Space Invaders code wipes memory on power-up, like a real arcade machine oughta
- [ ] Color overlay (toggleable in settings)
7 changes: 3 additions & 4 deletions cmd/cpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ var cpmCmd = &cobra.Command{
vm := emulator.NewEmulator(cpmHardware)
vm.StartInterruptRoutines()
vm.Logger = logger
vm.Options.UnlimitedTPS = true

ebiten.SetWindowTitle("cpm test")
if vm.Options.UnlimitedTPS {
ebiten.SetTPS(ebiten.SyncWithFPS)
} else {
if vm.Options.LimitTPS {
ebiten.SetTPS(60)
} else {
ebiten.SetTPS(ebiten.SyncWithFPS)
}
ebiten.SetWindowSize(vm.Hardware.Width()*vm.Hardware.Scale(), vm.Hardware.Height()*vm.Hardware.Scale())

Expand Down
Binary file added cmd/fonts/PressStart2P-Regular.ttf
Binary file not shown.
66 changes: 66 additions & 0 deletions cmd/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cmd

import (
"image/color"
"strings"

"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text/v2"
)

// HelpSection represents a static section in the menu to display game controls.
type HelpSection struct {
name string
controls []string
}

func (hs *HelpSection) Name() string {
return hs.name
}

func (hs *HelpSection) Value() interface{} {
return nil // HelpSection has no modifiable value
}

func (hs *HelpSection) SetValue(val interface{}) error {
return nil // HelpSection cannot be modified
}

func (hs *HelpSection) Render(screen *ebiten.Image, x, y int, selected bool) {
// Colors for the bindings and descriptions
bindingColor := color.RGBA{255, 255, 0, 255} // Yellow for the keybinding
descriptionColor := color.RGBA{255, 255, 255, 255} // White for the description

// Render the help section title
title := "Help - Game Controls"
op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y))
op.ColorScale.ScaleWithColor(color.RGBA{196, 167, 231, 255})
text.Draw(screen, title, loadedFont, op)

// Adjust the starting y-coordinate for the controls
y += 40 // Adjust spacing as needed

// Render each control in the list
for i, control := range hs.controls {
// Split the control into binding and description
parts := strings.SplitN(control, " - ", 2)
if len(parts) != 2 {
continue // Skip malformed entries
}
binding := parts[0]
description := parts[1]

// Render the keybinding
op = &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y+(i*30))) // Adjust line height as needed
op.ColorScale.ScaleWithColor(bindingColor)
text.Draw(screen, binding, loadedFont, op)

// Render the description in the second column
op = &text.DrawOptions{}
op.GeoM.Translate(float64(x+200), float64(y+(i*30))) // Adjust x+200 for column spacing
op.ColorScale.ScaleWithColor(descriptionColor)
text.Draw(screen, description, loadedFont, op)
}
}
Binary file added cmd/images/ship.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
262 changes: 262 additions & 0 deletions cmd/menu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package cmd

import (
"encoding/json"
"fmt"
"image/color"
"os"
"strings"

"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text/v2"
)

type MenuScreen struct {
settings []Setting
selectedIndex int
errorMessage string
settingsFile string
helpSection *HelpSection
}

func NewMenuScreen(settingsFile string) *MenuScreen {
ms := &MenuScreen{
settingsFile: settingsFile,
}
if err := ms.loadSettings(); err != nil {
ms.errorMessage = fmt.Sprintf("Error loading settings: %v", err)
ms.initializeDefaultSettings()
}
return ms
}

func (ms *MenuScreen) initializeDefaultSettings() {
// Clone the default settings to ms.settings
ms.settings = NewDefaultSettings()

// Initialize the help section separately
ms.helpSection = &HelpSection{
name: "Game Controls",
controls: []string{
"Arrow Keys/WASD - Move, Navigate menu",
"Space - Shoot",
"C - Insert credit",
"1 - Player 1 Start",
"2 - Player 2 Start",
"T - Tilt",
"Enter - Toggle setting",
"Tab - Toggle menu",
"Esc - Quit",
},
}
}

func (ms *MenuScreen) GetLimitTPS() bool {
for _, setting := range ms.settings {
if onOffSetting, ok := setting.(*OnOffSetting); ok && onOffSetting.name == "Limit to 60 FPS" {
return onOffSetting.value
}
}
return true
}

func (ms *MenuScreen) GetShipsSetting() int {
for _, setting := range ms.settings {
if rangeSetting, ok := setting.(*RangeSetting); ok && rangeSetting.name == "Ship Count" {
return rangeSetting.value
}
}
return 3 // Default to 3 ships if not found
}

func (ms *MenuScreen) GetExtraShipAt1000() bool {
for _, setting := range ms.settings {
if onOffSetting, ok := setting.(*OnOffSetting); ok && onOffSetting.name == "Extra ship at 1000 instead of 1500" {
return onOffSetting.value
}
}
return false // Default to extra ship at 1500
}

func (ms *MenuScreen) GetShowCoinInfoOnDemo() bool {
for _, setting := range ms.settings {
if onOffSetting, ok := setting.(*OnOffSetting); ok && onOffSetting.name == "Show coin info on demo screen" {
return onOffSetting.value
}
}
return true // Default to showing coin info
}

func (ms *MenuScreen) loadSettings() error {
file, err := os.Open(ms.settingsFile)
if os.IsNotExist(err) {
// If the file does not exist, initialize default settings and return nil
ms.initializeDefaultSettings()
return nil
} else if err != nil {
return err
}
defer file.Close()

decoder := json.NewDecoder(file)
loadedSettings := map[string]interface{}{}
if err := decoder.Decode(&loadedSettings); err != nil {
return err
}

ms.initializeDefaultSettings()

for _, setting := range ms.settings {
if value, ok := loadedSettings[setting.Name()]; ok {
if err := setting.SetValue(value); err != nil {
return err
}
}
}
return nil
}

func (ms *MenuScreen) saveSettings() error {
defaultSettings := NewDefaultSettings()
settingsMap := make(map[string]interface{})

for _, setting := range ms.settings {
for _, defaultSetting := range defaultSettings {
if setting.Name() == defaultSetting.Name() && setting.Value() != defaultSetting.Value() {
settingsMap[setting.Name()] = setting.Value()
break
}
}
}

if len(settingsMap) == 0 {
// No settings to save, skip creating or writing to the file
return nil
}

// Now proceed to create and save the file only if there are settings to save
file, err := os.Create(ms.settingsFile)
if err != nil {
return err
}
defer file.Close()

encoder := json.NewEncoder(file)
return encoder.Encode(settingsMap)
}

func (ms *MenuScreen) Update() {
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) {
ms.selectedIndex = (ms.selectedIndex + 1) % len(ms.settings) // Wrap around to the first setting
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
ms.selectedIndex--
if ms.selectedIndex < 0 {
ms.selectedIndex = len(ms.settings) - 1 // Wrap around to the last setting
}
}

if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
ms.toggleSelectedSetting()
}

// Handle left/right arrow keys for range settings
selectedSetting := ms.settings[ms.selectedIndex]
if rangeSetting, ok := selectedSetting.(*RangeSetting); ok {
if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) || inpututil.IsKeyJustPressed(ebiten.KeyA) {
if rangeSetting.value > rangeSetting.minVal {
rangeSetting.value--
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) || inpututil.IsKeyJustPressed(ebiten.KeyD) {
if rangeSetting.value < rangeSetting.maxVal {
rangeSetting.value++
}
}
}
}

func (ms *MenuScreen) toggleSelectedSetting() {

selectedSetting := ms.settings[ms.selectedIndex]
switch setting := selectedSetting.(type) {
case *OnOffSetting:
if ebiten.IsKeyPressed(ebiten.KeyEnter) {
setting.SetValue(!setting.value)
}
case *RangeSetting:
// Cycle through the range
newValue := setting.value + 1
if newValue > setting.maxVal {
newValue = setting.minVal
}
setting.SetValue(newValue)
}

}

func (ms *MenuScreen) Draw(screen *ebiten.Image) {
// Render the overall menu title
menuTitle := "Settings Menu"
titleOp := &text.DrawOptions{}
titleOp.GeoM.Translate(float64(50), float64(20)) // Position at the top of the screen
titleOp.ColorScale.ScaleWithColor(color.RGBA{196, 167, 231, 255})
text.Draw(screen, menuTitle, loadedFont, titleOp)

// Calculate the start position for drawing the settings
startX := 50
startY := 70 // Adjust startY to account for the title
lineHeight := 30

// Iterate through each setting and draw it
for i, setting := range ms.settings {
y := startY + (i * lineHeight) // Calculate the Y position for each setting
selected := i == ms.selectedIndex
setting.Render(screen, startX, y, selected)
}

// Draw the help section below the settings
if ms.helpSection != nil {
y := startY + (len(ms.settings) * lineHeight) + 50 // Space between settings and help section
ms.helpSection.Render(screen, startX, y, false)
}

// Draw the error message if it exists
if ms.errorMessage != "" {
errorColor := color.RGBA{255, 0, 0, 255}
msgX := 50
msgY := screen.Bounds().Dy() - 100 // Start drawing a bit higher for word wrapping

// Word wrap the error message
lines := wordWrap(ms.errorMessage, 50) // Wrap at 50 characters per line
for i, line := range lines {
op := &text.DrawOptions{}
op.GeoM.Translate(float64(msgX), float64(msgY+(i*lineHeight))) // Use consistent line height
op.ColorScale.ScaleWithColor(errorColor)
text.Draw(screen, line, loadedFont, op)
}
}
}

// Helper function to word wrap text
func wordWrap(text string, maxLineLength int) []string {
words := strings.Fields(text)
var lines []string
var currentLine string

for _, word := range words {
if len(currentLine)+len(word)+1 > maxLineLength {
lines = append(lines, currentLine)
currentLine = word
} else {
if len(currentLine) > 0 {
currentLine += " "
}
currentLine += word
}
}
if len(currentLine) > 0 {
lines = append(lines, currentLine)
}

return lines
}
Loading

0 comments on commit 0555468

Please sign in to comment.