diff --git a/d2common/d2enum/hero.go b/d2common/d2enum/hero.go index b45d5901..b68a4a64 100644 --- a/d2common/d2enum/hero.go +++ b/d2common/d2enum/hero.go @@ -3,12 +3,15 @@ package d2enum type Hero int const ( - HeroNone Hero = 0 - HeroBarbarian Hero = 1 - HeroNecromancer Hero = 2 - HeroPaladin Hero = 3 - HeroAssassin Hero = 4 - HeroSorceress Hero = 5 - HeroAmazon Hero = 6 - HeroDruid Hero = 7 + HeroNone Hero = 0 // + HeroBarbarian Hero = 1 // Barbarian + HeroNecromancer Hero = 2 // Necromancer + HeroPaladin Hero = 3 // Paladin + HeroAssassin Hero = 4 // Assassin + HeroSorceress Hero = 5 // Sorceress + HeroAmazon Hero = 6 // Amazon + HeroDruid Hero = 7 // Druid ) + +//go:generate stringer -linecomment -type Hero +//go:generate string2enum -samepkg -linecomment -type Hero diff --git a/d2common/d2enum/hero_string.go b/d2common/d2enum/hero_string.go new file mode 100644 index 00000000..d185b15c --- /dev/null +++ b/d2common/d2enum/hero_string.go @@ -0,0 +1,30 @@ +// Code generated by "stringer -linecomment -type Hero"; DO NOT EDIT. + +package d2enum + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[HeroNone-0] + _ = x[HeroBarbarian-1] + _ = x[HeroNecromancer-2] + _ = x[HeroPaladin-3] + _ = x[HeroAssassin-4] + _ = x[HeroSorceress-5] + _ = x[HeroAmazon-6] + _ = x[HeroDruid-7] +} + +const _Hero_name = "BarbarianNecromancerPaladinAssassinSorceressAmazonDruid" + +var _Hero_index = [...]uint8{0, 0, 9, 20, 27, 35, 44, 50, 55} + +func (i Hero) String() string { + if i < 0 || i >= Hero(len(_Hero_index)-1) { + return "Hero(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Hero_name[_Hero_index[i]:_Hero_index[i+1]] +} diff --git a/d2common/d2enum/hero_string2enum.go b/d2common/d2enum/hero_string2enum.go new file mode 100644 index 00000000..992ac5f3 --- /dev/null +++ b/d2common/d2enum/hero_string2enum.go @@ -0,0 +1,18 @@ +// Code generated by "string2enum -samepkg -linecomment -type Hero"; DO NOT EDIT. + +package d2enum + +import "fmt" + +// HeroFromString returns the Hero enum corresponding to s. +func HeroFromString(s string) Hero { + if len(s) == 0 { + return 0 + } + for i := range _Hero_index[:len(_Hero_index)-1] { + if s == _Hero_name[_Hero_index[i]:_Hero_index[i+1]] { + return Hero(i) + } + } + panic(fmt.Errorf("unable to locate Hero enum corresponding to %q", s)) +} diff --git a/d2common/d2enum/region_id.go b/d2common/d2enum/region_id.go new file mode 100644 index 00000000..2bd4f23b --- /dev/null +++ b/d2common/d2enum/region_id.go @@ -0,0 +1,41 @@ +package d2enum + +type RegionIdType int + +const ( + RegionAct1Town RegionIdType = 1 + RegionAct1Wilderness RegionIdType = 2 + RegionAct1Cave RegionIdType = 3 + RegionAct1Crypt RegionIdType = 4 + RegionAct1Monestary RegionIdType = 5 + RegionAct1Courtyard RegionIdType = 6 + RegionAct1Barracks RegionIdType = 7 + RegionAct1Jail RegionIdType = 8 + RegionAct1Cathedral RegionIdType = 9 + RegionAct1Catacombs RegionIdType = 10 + RegionAct1Tristram RegionIdType = 11 + RegionAct2Town RegionIdType = 12 + RegionAct2Sewer RegionIdType = 13 + RegionAct2Harem RegionIdType = 14 + RegionAct2Basement RegionIdType = 15 + RegionAct2Desert RegionIdType = 16 + RegionAct2Tomb RegionIdType = 17 + RegionAct2Lair RegionIdType = 18 + RegionAct2Arcane RegionIdType = 19 + RegionAct3Town RegionIdType = 20 + RegionAct3Jungle RegionIdType = 21 + RegionAct3Kurast RegionIdType = 22 + RegionAct3Spider RegionIdType = 23 + RegionAct3Dungeon RegionIdType = 24 + RegionAct3Sewer RegionIdType = 25 + RegionAct4Town RegionIdType = 26 + RegionAct4Mesa RegionIdType = 27 + RegionAct4Lava RegionIdType = 28 + RegonAct5Town RegionIdType = 29 + RegionAct5Siege RegionIdType = 30 + RegionAct5Barricade RegionIdType = 31 + RegionAct5Temple RegionIdType = 32 + RegionAct5IceCaves RegionIdType = 33 + RegionAct5Baal RegionIdType = 34 + RegionAct5Lava RegionIdType = 35 +) diff --git a/d2common/d2enum/region_layer.go b/d2common/d2enum/region_layer.go new file mode 100644 index 00000000..749f7739 --- /dev/null +++ b/d2common/d2enum/region_layer.go @@ -0,0 +1,9 @@ +package d2enum + +type RegionLayerType int + +const ( + RegionLayerTypeFloors RegionLayerType = 0 + RegionLayerTypeWalls RegionLayerType = 1 + RegionLayerTypeShadows RegionLayerType = 2 +) diff --git a/d2common/d2resource/resource_paths.go b/d2common/d2resource/resource_paths.go index 34b5fe37..623ecc0b 100644 --- a/d2common/d2resource/resource_paths.go +++ b/d2common/d2resource/resource_paths.go @@ -80,6 +80,8 @@ const ( // -- Character Selection CharacterSelectionBackground = "/data/global/ui/CharSelect/characterselectscreenEXP.dc6" + CharacterSelectionSelectBox = "/data/global/ui/CharSelect/charselectbox.dc6" + PopUpOkCancel = "/data/global/ui/FrontEnd/PopUpOKCancel.dc6" // --- Game --- @@ -101,6 +103,7 @@ const ( Font16 = "/data/local/font/latin/font16" Font24 = "/data/local/font/latin/font24" Font30 = "/data/local/font/latin/font30" + Font42 = "/data/local/font/latin/font42" FontFormal12 = "/data/local/font/latin/fontformal12" FontFormal11 = "/data/local/font/latin/fontformal11" FontFormal10 = "/data/local/font/latin/fontformal10" diff --git a/d2common/stream_reader.go b/d2common/stream_reader.go index ddb81c7d..436670e3 100644 --- a/d2common/stream_reader.go +++ b/d2common/stream_reader.go @@ -55,20 +55,48 @@ func (v *StreamReader) SetPosition(newPosition uint64) { v.position = newPosition } -// GetUInt32 returns a uint32 word from the stream +// GetUInt32 returns a uint32 dword from the stream func (v *StreamReader) GetUInt32() uint32 { result := (uint32(v.data[v.position+3]) << uint(24)) + (uint32(v.data[v.position+2]) << uint(16)) + (uint32(v.data[v.position+1]) << uint(8)) + uint32(v.data[v.position]) v.position += 4 return result } -// GetInt32 returns an int32 word from the stream +// GetInt32 returns an int32 dword from the stream func (v *StreamReader) GetInt32() int32 { result := (int32(v.data[v.position+3]) << uint(24)) + (int32(v.data[v.position+2]) << uint(16)) + (int32(v.data[v.position+1]) << uint(8)) + int32(v.data[v.position]) v.position += 4 return result } +// GetUint64 returns a uint64 qword from the stream +func (v *StreamReader) GetUint64() uint64 { + result := (uint64(v.data[v.position+7]) << uint(56)) + + (uint64(v.data[v.position+6]) << uint(48)) + + (uint64(v.data[v.position+5]) << uint(40)) + + (uint64(v.data[v.position+4]) << uint(32)) + + (uint64(v.data[v.position+3]) << uint(24)) + + (uint64(v.data[v.position+2]) << uint(16)) + + (uint64(v.data[v.position+1]) << uint(8)) + + uint64(v.data[v.position]) + v.position += 8 + return result +} + +// GetInt64 returns a uint64 qword from the stream +func (v *StreamReader) GetInt64() int64 { + result := (uint64(v.data[v.position+7]) << uint(56)) + + (uint64(v.data[v.position+6]) << uint(48)) + + (uint64(v.data[v.position+5]) << uint(40)) + + (uint64(v.data[v.position+4]) << uint(32)) + + (uint64(v.data[v.position+3]) << uint(24)) + + (uint64(v.data[v.position+2]) << uint(16)) + + (uint64(v.data[v.position+1]) << uint(8)) + + uint64(v.data[v.position]) + v.position += 8 + return int64(result) +} + // ReadByte implements io.ByteReader func (v *StreamReader) ReadByte() (byte, error) { return v.GetByte(), nil diff --git a/d2common/stream_writer.go b/d2common/stream_writer.go index 8d2ffa97..6c10530b 100644 --- a/d2common/stream_writer.go +++ b/d2common/stream_writer.go @@ -18,26 +18,51 @@ func (v *StreamWriter) PushByte(val byte) { v.data = append(v.data, val) } -// PushWord writes an uint16 word to the stream -func (v *StreamWriter) PushWord(val uint16) { +// PushUint16 writes an uint16 word to the stream +func (v *StreamWriter) PushUint16(val uint16) { v.data = append(v.data, byte(val&0xFF)) v.data = append(v.data, byte((val>>8)&0xFF)) } -// PushSWord writes a int16 word to the stream -func (v *StreamWriter) PushSWord(val int16) { +// PushInt16 writes a int16 word to the stream +func (v *StreamWriter) PushInt16(val int16) { v.data = append(v.data, byte(val&0xFF)) v.data = append(v.data, byte((val>>8)&0xFF)) } -// PushDword writes a uint32 dword to the stream -func (v *StreamWriter) PushDword(val uint32) { +// PushUint32 writes a uint32 dword to the stream +func (v *StreamWriter) PushUint32(val uint32) { v.data = append(v.data, byte(val&0xFF)) v.data = append(v.data, byte((val>>8)&0xFF)) v.data = append(v.data, byte((val>>16)&0xFF)) v.data = append(v.data, byte((val>>24)&0xFF)) } +// PushUint64 writes a uint64 qword to the stream +func (v *StreamWriter) PushUint64(val uint64) { + v.data = append(v.data, byte(val&0xFF)) + v.data = append(v.data, byte((val>>8)&0xFF)) + v.data = append(v.data, byte((val>>16)&0xFF)) + v.data = append(v.data, byte((val>>24)&0xFF)) + v.data = append(v.data, byte((val>>32)&0xFF)) + v.data = append(v.data, byte((val>>40)&0xFF)) + v.data = append(v.data, byte((val>>48)&0xFF)) + v.data = append(v.data, byte((val>>56)&0xFF)) +} + +// PushInt64 writes a uint64 qword to the stream +func (v *StreamWriter) PushInt64(val int64) { + result := uint64(val) + v.data = append(v.data, byte(result&0xFF)) + v.data = append(v.data, byte((result>>8)&0xFF)) + v.data = append(v.data, byte((result>>16)&0xFF)) + v.data = append(v.data, byte((result>>24)&0xFF)) + v.data = append(v.data, byte((result>>32)&0xFF)) + v.data = append(v.data, byte((result>>40)&0xFF)) + v.data = append(v.data, byte((result>>48)&0xFF)) + v.data = append(v.data, byte((result>>56)&0xFF)) +} + // GetBytes returns the the byte slice of the underlying data func (v *StreamWriter) GetBytes() []byte { return v.data diff --git a/d2common/stream_writer_test.go b/d2common/stream_writer_test.go index e73343b8..084404f5 100644 --- a/d2common/stream_writer_test.go +++ b/d2common/stream_writer_test.go @@ -21,8 +21,8 @@ func TestStreamWriterByte(t *testing.T) { func TestStreamWriterWord(t *testing.T) { sr := CreateStreamWriter() data := []byte{0x12, 0x34, 0x56, 0x78} - sr.PushWord(0x3412) - sr.PushWord(0x7856) + sr.PushUint16(0x3412) + sr.PushUint16(0x7856) output := sr.GetBytes() for i, d := range data { if output[i] != d { @@ -34,7 +34,7 @@ func TestStreamWriterWord(t *testing.T) { func TestStreamWriterDword(t *testing.T) { sr := CreateStreamWriter() data := []byte{0x12, 0x34, 0x56, 0x78} - sr.PushDword(0x78563412) + sr.PushUint32(0x78563412) output := sr.GetBytes() for i, d := range data { if output[i] != d { diff --git a/d2core/d2scene/character_select.go b/d2core/d2scene/character_select.go index dc3c7cae..f3437a21 100644 --- a/d2core/d2scene/character_select.go +++ b/d2core/d2scene/character_select.go @@ -1,11 +1,18 @@ package d2scene import ( + "image/color" + "os" + "strings" + + "github.com/hajimehoshi/ebiten/ebitenutil" + "github.com/OpenDiablo2/OpenDiablo2/d2audio" "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2core" "github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict" dh "github.com/OpenDiablo2/OpenDiablo2/d2helper" "github.com/OpenDiablo2/OpenDiablo2/d2render" @@ -14,16 +21,31 @@ import ( ) type CharacterSelect struct { - uiManager *d2ui.Manager - soundManager *d2audio.Manager - fileProvider d2interface.FileProvider - sceneProvider d2interface.SceneProvider - background d2render.Sprite - newCharButton d2ui.Button - convertCharButton d2ui.Button - deleteCharButton d2ui.Button - exitButton d2ui.Button - okButton d2ui.Button + uiManager *d2ui.Manager + soundManager *d2audio.Manager + fileProvider d2interface.FileProvider + sceneProvider d2interface.SceneProvider + background d2render.Sprite + newCharButton d2ui.Button + convertCharButton d2ui.Button + deleteCharButton d2ui.Button + exitButton d2ui.Button + okButton d2ui.Button + deleteCharCancelButton d2ui.Button + deleteCharOkButton d2ui.Button + selectionBox d2render.Sprite + okCancelBox d2render.Sprite + d2HeroTitle d2ui.Label + deleteCharConfirmLabel d2ui.Label + characterNameLabel [8]d2ui.Label + characterStatsLabel [8]d2ui.Label + characterExpLabel [8]d2ui.Label + characterImage [8]*d2core.NPC + gameStates []*d2core.GameState + scrollOffset int + selectedCharacter int + mouseButtonPressed bool + showDeleteConfirmation bool } func CreateCharacterSelect( @@ -33,10 +55,11 @@ func CreateCharacterSelect( soundManager *d2audio.Manager, ) *CharacterSelect { result := &CharacterSelect{ - uiManager: uiManager, - sceneProvider: sceneProvider, - fileProvider: fileProvider, - soundManager: soundManager, + selectedCharacter: -1, + uiManager: uiManager, + sceneProvider: sceneProvider, + fileProvider: fileProvider, + soundManager: soundManager, } return result } @@ -62,8 +85,8 @@ func (v *CharacterSelect) Load() []func() { }, func() { v.deleteCharButton = d2ui.CreateButton(d2ui.ButtonTypeTall, v.fileProvider, dh.CombineStrings(dh.SplitIntoLinesWithMaxWidth(d2common.TranslateString("#832"), 15))) + v.deleteCharButton.OnActivated(func() { v.onDeleteCharButtonClicked() }) v.deleteCharButton.MoveTo(433, 468) - v.deleteCharButton.SetEnabled(false) v.uiManager.AddWidget(&v.deleteCharButton) }, func() { @@ -72,12 +95,81 @@ func (v *CharacterSelect) Load() []func() { v.exitButton.OnActivated(func() { v.onExitButtonClicked() }) v.uiManager.AddWidget(&v.exitButton) }, + func() { + v.deleteCharCancelButton = d2ui.CreateButton(d2ui.ButtonTypeOkCancel, v.fileProvider, d2common.TranslateString("#4231")) + v.deleteCharCancelButton.MoveTo(282, 308) + v.deleteCharCancelButton.SetVisible(false) + v.deleteCharCancelButton.OnActivated(func() { v.onDeleteCharacterCancelClicked() }) + v.uiManager.AddWidget(&v.deleteCharCancelButton) + }, + func() { + v.deleteCharOkButton = d2ui.CreateButton(d2ui.ButtonTypeOkCancel, v.fileProvider, d2common.TranslateString("#4227")) + v.deleteCharOkButton.MoveTo(422, 308) + v.deleteCharOkButton.SetVisible(false) + v.deleteCharOkButton.OnActivated(func() { v.onDeleteCharacterConfirmClicked() }) + v.uiManager.AddWidget(&v.deleteCharOkButton) + }, func() { v.okButton = d2ui.CreateButton(d2ui.ButtonTypeMedium, v.fileProvider, d2common.TranslateString("#971")) v.okButton.MoveTo(625, 537) - v.okButton.SetEnabled(false) v.uiManager.AddWidget(&v.okButton) }, + func() { + v.d2HeroTitle = d2ui.CreateLabel(v.fileProvider, d2resource.Font42, d2enum.Units) + v.d2HeroTitle.MoveTo(320, 23) + v.d2HeroTitle.Alignment = d2ui.LabelAlignCenter + }, + func() { + v.deleteCharConfirmLabel = d2ui.CreateLabel(v.fileProvider, d2resource.Font16, d2enum.Units) + lines := dh.SplitIntoLinesWithMaxWidth(d2common.TranslateString("#1878"), 29) + v.deleteCharConfirmLabel.SetText(strings.Join(lines, "\n")) + v.deleteCharConfirmLabel.Alignment = d2ui.LabelAlignCenter + v.deleteCharConfirmLabel.MoveTo(400, 185) + }, + func() { + v.selectionBox = d2render.CreateSprite(v.fileProvider.LoadFile(d2resource.CharacterSelectionSelectBox), d2datadict.Palettes[d2enum.Sky]) + v.selectionBox.MoveTo(37, 86) + }, + func() { + v.okCancelBox = d2render.CreateSprite(v.fileProvider.LoadFile(d2resource.PopUpOkCancel), d2datadict.Palettes[d2enum.Fechar]) + v.okCancelBox.MoveTo(270, 175) + }, + func() { + for i := 0; i < 8; i++ { + xOffset := 115 + if i&1 > 0 { + xOffset = 385 + } + v.characterNameLabel[i] = d2ui.CreateLabel(v.fileProvider, d2resource.Font16, d2enum.Units) + v.characterNameLabel[i].Color = color.RGBA{188, 168, 140, 255} + v.characterNameLabel[i].MoveTo(xOffset, 100+((i/2)*95)) + v.characterStatsLabel[i] = d2ui.CreateLabel(v.fileProvider, d2resource.Font16, d2enum.Units) + v.characterStatsLabel[i].MoveTo(xOffset, 115+((i/2)*95)) + v.characterExpLabel[i] = d2ui.CreateLabel(v.fileProvider, d2resource.Font16, d2enum.Static) + v.characterExpLabel[i].Color = color.RGBA{24, 255, 0, 255} + v.characterExpLabel[i].MoveTo(xOffset, 130+((i/2)*95)) + } + v.refreshGameStates() + }, + } +} + +func (v *CharacterSelect) updateCharacterBoxes() { + expText := d2common.TranslateString("expansionchar2x") + for i := 0; i < 8; i++ { + idx := i + (v.scrollOffset * 2) + if idx >= len(v.gameStates) { + v.characterNameLabel[i].SetText("") + v.characterStatsLabel[i].SetText("") + v.characterExpLabel[i].SetText("") + v.characterImage[i] = nil + continue + } + v.characterNameLabel[i].SetText(v.gameStates[idx].HeroName) + v.characterStatsLabel[i].SetText("Level 1 " + v.gameStates[idx].HeroType.String()) + v.characterExpLabel[i].SetText(expText) + // TODO: Generate or load the object from the actual player data... + v.characterImage[i] = d2core.CreateNPC(0, 0, d2datadict.HeroObjects[v.gameStates[idx].HeroType], v.fileProvider, 5) } } @@ -96,7 +188,100 @@ func (v *CharacterSelect) Unload() { func (v *CharacterSelect) Render(screen *ebiten.Image) { v.background.DrawSegments(screen, 4, 3, 0) + v.d2HeroTitle.Draw(screen) + if v.selectedCharacter > -1 { + v.selectionBox.DrawSegments(screen, 2, 1, 0) + } + for i := 0; i < 8; i++ { + idx := i + (v.scrollOffset * 2) + if idx >= len(v.gameStates) { + continue + } + v.characterNameLabel[i].Draw(screen) + v.characterStatsLabel[i].Draw(screen) + v.characterExpLabel[i].Draw(screen) + + v.characterImage[i].Render(screen, v.characterNameLabel[i].X-40, v.characterNameLabel[i].Y+30) + } + if v.showDeleteConfirmation { + ebitenutil.DrawRect(screen, 0.0, 0.0, 800.0, 600.0, color.RGBA{0, 0, 0, 128}) + v.okCancelBox.DrawSegments(screen, 2, 1, 0) + v.deleteCharConfirmLabel.Draw(screen) + } +} + +func (v *CharacterSelect) moveSelectionBox() { + bw := 272 + bh := 92 + selectedIndex := v.selectedCharacter - (v.scrollOffset * 2) + v.selectionBox.MoveTo(37+((selectedIndex&1)*int(bw)), 86+(int(bh)*(selectedIndex/2))) + v.d2HeroTitle.SetText(v.gameStates[v.selectedCharacter].HeroName) } func (v *CharacterSelect) Update(tickTime float64) { + if !v.showDeleteConfirmation { + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + if !v.mouseButtonPressed { + v.mouseButtonPressed = true + mx, my := ebiten.CursorPosition() + bw := 272 + bh := 92 + localMouseX := mx - 37 + localMouseY := my - 86 + if localMouseX > 0 && localMouseX < int(bw*2) && localMouseY >= 0 && localMouseY < int(bh*4) { + adjustY := localMouseY / int(bh) + selectedIndex := adjustY * 2 + if localMouseX > int(bw) { + selectedIndex += 1 + } + if (v.scrollOffset*2)+selectedIndex < len(v.gameStates) { + v.selectedCharacter = (v.scrollOffset * 2) + selectedIndex + v.moveSelectionBox() + } + } + } + } else { + v.mouseButtonPressed = false + } + } +} + +func (v *CharacterSelect) onDeleteCharButtonClicked() { + v.toggleDeleteCharacterDialog(true) +} + +func (v *CharacterSelect) onDeleteCharacterConfirmClicked() { + _ = os.Remove(v.gameStates[v.selectedCharacter].FilePath) + v.selectedCharacter = -1 + v.refreshGameStates() + v.toggleDeleteCharacterDialog(false) +} + +func (v *CharacterSelect) onDeleteCharacterCancelClicked() { + v.toggleDeleteCharacterDialog(false) +} + +func (v *CharacterSelect) toggleDeleteCharacterDialog(showDialog bool) { + v.showDeleteConfirmation = showDialog + v.okButton.SetEnabled(!showDialog) + v.deleteCharButton.SetEnabled(!showDialog) + v.exitButton.SetEnabled(!showDialog) + v.newCharButton.SetEnabled(!showDialog) + v.deleteCharOkButton.SetVisible(showDialog) + v.deleteCharCancelButton.SetVisible(showDialog) +} + +func (v *CharacterSelect) refreshGameStates() { + v.gameStates = d2core.GetAllGameStates() + v.updateCharacterBoxes() + if len(v.gameStates) > 0 { + v.selectedCharacter = 0 + v.d2HeroTitle.SetText(v.gameStates[0].HeroName) + } else { + v.selectedCharacter = -1 + } + v.deleteCharButton.SetEnabled(v.selectedCharacter > -1) + v.okButton.SetEnabled(v.selectedCharacter > -1) + v.moveSelectionBox() + } diff --git a/d2core/d2scene/game.go b/d2core/d2scene/game.go new file mode 100644 index 00000000..7866d4ed --- /dev/null +++ b/d2core/d2scene/game.go @@ -0,0 +1,54 @@ +package d2scene + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2audio" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2core" + "github.com/OpenDiablo2/OpenDiablo2/d2render/d2ui" + "github.com/hajimehoshi/ebiten" +) + +type Game struct { + gameState *d2core.GameState + uiManager *d2ui.Manager + soundManager *d2audio.Manager + fileProvider d2interface.FileProvider + sceneProvider d2interface.SceneProvider +} + +func CreateGame( + fileProvider d2interface.FileProvider, + sceneProvider d2interface.SceneProvider, + uiManager *d2ui.Manager, + soundManager *d2audio.Manager, + gameState *d2core.GameState, +) *Game { + result := &Game{ + gameState: gameState, + fileProvider: fileProvider, + uiManager: uiManager, + soundManager: soundManager, + sceneProvider: sceneProvider, + } + return result +} + +func (v *Game) Load() []func() { + return []func(){ + func() { + + }, + } +} + +func (v *Game) Unload() { + +} + +func (v Game) Render(screen *ebiten.Image) { + +} + +func (v *Game) Update(tickTime float64) { + +} diff --git a/d2core/d2scene/main_menu.go b/d2core/d2scene/main_menu.go index 4b098dfd..ef4fbde2 100644 --- a/d2core/d2scene/main_menu.go +++ b/d2core/d2scene/main_menu.go @@ -8,6 +8,8 @@ import ( "os/exec" "runtime" + "github.com/OpenDiablo2/OpenDiablo2/d2core" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict" @@ -202,6 +204,10 @@ func openbrowser(url string) { func (v *MainMenu) onSinglePlayerClicked() { // Go here only if existing characters are available to select + if d2core.HasGameStates() { + v.sceneProvider.SetNextScene(CreateCharacterSelect(v.fileProvider, v.sceneProvider, v.uiManager, v.soundManager)) + return + } v.sceneProvider.SetNextScene(CreateSelectHeroClass(v.fileProvider, v.sceneProvider, v.uiManager, v.soundManager)) } diff --git a/d2core/d2scene/map_engine_testing.go b/d2core/d2scene/map_engine_testing.go index 7dc0e533..6def4703 100644 --- a/d2core/d2scene/map_engine_testing.go +++ b/d2core/d2scene/map_engine_testing.go @@ -37,7 +37,7 @@ func CreateMapEngineTest( soundManager: soundManager, sceneProvider: sceneProvider, } - result.gameState = d2core.CreateGameState() + result.gameState = d2core.CreateTestGameState() return result } diff --git a/d2core/d2scene/select_hero_class.go b/d2core/d2scene/select_hero_class.go index bb8e9642..594f13dd 100644 --- a/d2core/d2scene/select_hero_class.go +++ b/d2core/d2scene/select_hero_class.go @@ -4,6 +4,8 @@ import ( "image" "image/color" + "github.com/OpenDiablo2/OpenDiablo2/d2core" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict" @@ -420,6 +422,10 @@ func (v *SelectHeroClass) Load() []func() { } func (v *SelectHeroClass) Unload() { + for i := range v.heroRenderInfo { + v.heroRenderInfo[i].SelectSfx.Stop() + v.heroRenderInfo[i].DeselectSfx.Stop() + } v.heroRenderInfo = nil } @@ -428,7 +434,8 @@ func (v SelectHeroClass) onExitButtonClicked() { } func (v SelectHeroClass) onOkButtonClicked() { - // TODO: Start the game + gameState := d2core.CreateGameState(v.heroNameTextbox.GetText(), v.selectedHero, v.hardcoreCheckbox.GetCheckState()) + v.sceneProvider.SetNextScene(CreateGame(v.fileProvider, v.sceneProvider, v.uiManager, v.soundManager, gameState)) } func (v *SelectHeroClass) Render(screen *ebiten.Image) { diff --git a/d2core/engine.go b/d2core/engine.go index 80cf8be9..eaf78b29 100644 --- a/d2core/engine.go +++ b/d2core/engine.go @@ -226,7 +226,7 @@ func (v Engine) Draw(screen *ebiten.Image) { v.CurrentScene.Render(screen) v.UIManager.Draw(screen) } - if v.showFPS == true { + if v.showFPS { ebitenutil.DebugPrintAt(screen, "vsync:"+strconv.FormatBool(ebiten.IsVsyncEnabled())+"\nFPS:"+strconv.Itoa(int(ebiten.CurrentFPS())), 5, 565) var m runtime.MemStats runtime.ReadMemStats(&m) diff --git a/d2core/game_state.go b/d2core/game_state.go index fa50229b..b8658f10 100644 --- a/d2core/game_state.go +++ b/d2core/game_state.go @@ -1,15 +1,68 @@ package d2core import ( + "io/ioutil" "log" + "os" + "path" + "runtime" + "strconv" + "strings" "time" + + "github.com/OpenDiablo2/OpenDiablo2/d2common" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" ) +/* + File Spec + -------------------------------------------- + UINT32 GameState Version + INT64 Game Seed + BYTE Hero Type + BYTE Act + BYTE Hero Name Length + BYTE[] Hero Name + -------------------------------------------- +*/ + type GameState struct { - Seed int64 + Seed int64 + HeroName string + HeroType d2enum.Hero + Act int + FilePath string } -func CreateGameState() *GameState { +const GameStateVersion = uint32(1) // Update this when you make breaking changes + +func HasGameStates() bool { + files, _ := ioutil.ReadDir(getGameBaseSavePath()) + return len(files) > 0 +} + +func GetAllGameStates() []*GameState { + // TODO: Make this not crash tf out on bad files + basePath := getGameBaseSavePath() + files, _ := ioutil.ReadDir(basePath) + result := make([]*GameState, 0) + for _, file := range files { + fileName := file.Name() + if file.IsDir() || len(fileName) < 5 || strings.ToLower(fileName[len(fileName)-4:]) != ".od2" { + continue + } + gameState := LoadGameState(path.Join(basePath, file.Name())) + if gameState == nil { + continue + } + result = append(result, gameState) + } + return result +} + +// CreateTestGameState is used for the map engine previewer +func CreateTestGameState() *GameState { result := &GameState{ Seed: time.Now().UnixNano(), } @@ -17,6 +70,95 @@ func CreateGameState() *GameState { } func LoadGameState(path string) *GameState { - log.Fatal("LoadGameState not implemented.") - return nil + result := &GameState{ + FilePath: path, + } + f, err := os.Open(path) + if err != nil { + log.Panicf(err.Error()) + } + bytes, err := ioutil.ReadAll(f) + if err != nil { + log.Panicf(err.Error()) + } + sr := d2common.CreateStreamReader(bytes) + if sr.GetUInt32() > GameStateVersion { + // Unknown game version + return nil + } + result.Seed = sr.GetInt64() + result.HeroType = d2enum.Hero(sr.GetByte()) + result.Act = int(sr.GetByte()) + heroNameLen := sr.GetByte() + heroName, _ := sr.ReadBytes(int(heroNameLen)) + result.HeroName = string(heroName) + return result +} + +func CreateGameState(heroName string, hero d2enum.Hero, hardcore bool) *GameState { + result := &GameState{ + HeroName: heroName, + HeroType: hero, + Act: 1, + Seed: time.Now().UnixNano(), + FilePath: "", + } + result.Save() + return result +} + +func getGameBaseSavePath() string { + if runtime.GOOS == "windows" { + appDataPath := os.Getenv("APPDATA") + basePath := path.Join(appDataPath, "OpenDiablo2", "Saves") + if err := os.MkdirAll(basePath, os.ModeDir); err != nil { + log.Panicf(err.Error()) + } + return basePath + } + homeDir, err := os.UserHomeDir() + if err != nil { + log.Panicf(err.Error()) + } + basePath := path.Join(homeDir, ".OpenDiablo2", "Saves") + if err := os.MkdirAll(basePath, os.ModeDir); err != nil { + log.Panicf(err.Error()) + } + // TODO: Is mac supposed to have a super special place for the save games? + return basePath +} + +func getFirstFreefileName() string { + i := 0 + basePath := getGameBaseSavePath() + for { + filePath := path.Join(basePath, strconv.Itoa(i)+".od2") + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return filePath + } + i++ + } +} + +func (v *GameState) Save() { + if v.FilePath == "" { + v.FilePath = getFirstFreefileName() + } + f, err := os.Create(v.FilePath) + if err != nil { + log.Panicf(err.Error()) + } + defer f.Close() + sr := d2common.CreateStreamWriter() + sr.PushUint32(GameStateVersion) + sr.PushInt64(v.Seed) + sr.PushByte(byte(v.HeroType)) + sr.PushByte(byte(v.Act)) + sr.PushByte(byte(len(v.HeroName))) + for _, ch := range v.HeroName { + sr.PushByte(byte(ch)) + } + if _, err := f.Write(sr.GetBytes()); err != nil { + log.Panicf(err.Error()) + } } diff --git a/d2core/npc.go b/d2core/npc.go index e77faa16..8e88bb9a 100644 --- a/d2core/npc.go +++ b/d2core/npc.go @@ -4,7 +4,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - "github.com/OpenDiablo2/OpenDiablo2/d2data" + "github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2render" "github.com/hajimehoshi/ebiten" ) @@ -14,15 +14,18 @@ type NPC struct { Paths []d2common.Path } -func CreateNPC(object d2data.Object, fileProvider d2interface.FileProvider) *NPC { +func CreateNPC(x, y int32, object *d2datadict.ObjectLookupRecord, fileProvider d2interface.FileProvider, direction int) *NPC { result := &NPC{ - AnimatedEntity: d2render.CreateAnimatedEntity(object, fileProvider, d2enum.Units), - Paths: object.Paths, + AnimatedEntity: d2render.CreateAnimatedEntity(x, y, object, fileProvider, d2enum.Units), } - result.AnimatedEntity.SetMode(object.Lookup.Mode, object.Lookup.Class, 1, fileProvider) + result.AnimatedEntity.SetMode(object.Mode, object.Class, direction, fileProvider) return result } +func (v *NPC) SetPaths(paths []d2common.Path) { + v.Paths = paths +} + func (v *NPC) Render(target *ebiten.Image, offsetX, offsetY int) { v.AnimatedEntity.Render(target, offsetX, offsetY) } diff --git a/d2data/d2compression/wav.go b/d2data/d2compression/wav.go index d74411f3..2b5474f4 100644 --- a/d2data/d2compression/wav.go +++ b/d2data/d2compression/wav.go @@ -39,7 +39,7 @@ func WavDecompress(data []byte, channelCount int) []byte { for i := 0; i < channelCount; i++ { temp := input.GetInt16() Array2[i] = int(temp) - output.PushSWord(temp) + output.PushInt16(temp) } channel := channelCount - 1 @@ -56,7 +56,7 @@ func WavDecompress(data []byte, channelCount int) []byte { if Array1[channel] != 0 { Array1[channel]-- } - output.PushSWord(int16(Array2[channel])) + output.PushInt16(int16(Array2[channel])) break case 1: Array1[channel] += 8 @@ -115,7 +115,7 @@ func WavDecompress(data []byte, channelCount int) []byte { } } Array2[channel] = temp3 - output.PushSWord(int16(temp3)) + output.PushInt16(int16(temp3)) Array1[channel] += sLookup2[value&0x1f] if Array1[channel] < 0 { diff --git a/d2data/d2datadict/hero_objects.go b/d2data/d2datadict/hero_objects.go new file mode 100644 index 00000000..86ee6792 --- /dev/null +++ b/d2data/d2datadict/hero_objects.go @@ -0,0 +1,43 @@ +package d2datadict + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" +) + +var HeroObjects = map[d2enum.Hero]*ObjectLookupRecord{ + d2enum.HeroBarbarian: &ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: "BA", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", + }, + d2enum.HeroNecromancer: &ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: "NE", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", + }, + d2enum.HeroPaladin: &ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: "PA", Class: "1HS", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", + }, + d2enum.HeroAssassin: &ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: "AI", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", + }, + d2enum.HeroSorceress: &ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: "SO", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", + }, + d2enum.HeroAmazon: &ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: "AM", Class: "1HT", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", + }, + d2enum.HeroDruid: &ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: "DZ", Class: "HTH", HD: "LIT", TR: "LIT", LG: "LIT", RA: "LIT", LA: "LIT", RH: "", LH: "", + }, +} diff --git a/d2render/animated_entity.go b/d2render/animated_entity.go index 194e83e5..cdd0e447 100644 --- a/d2render/animated_entity.go +++ b/d2render/animated_entity.go @@ -48,20 +48,20 @@ type AnimatedEntity struct { currentFrame int frames map[string][]*ebiten.Image frameLocations map[string][]d2common.Rectangle - object d2data.Object + object *d2datadict.ObjectLookupRecord } // CreateAnimatedEntity creates an instance of AnimatedEntity -func CreateAnimatedEntity(object d2data.Object, fileProvider d2interface.FileProvider, palette d2enum.PaletteType) AnimatedEntity { +func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, fileProvider d2interface.FileProvider, palette d2enum.PaletteType) AnimatedEntity { result := AnimatedEntity{ - base: object.Lookup.Base, - token: object.Lookup.Token, + base: object.Base, + token: object.Token, object: object, palette: palette, } result.dccLayers = make(map[string]d2dcc.DCC) - result.LocationX = float64(object.X) / 5 - result.LocationY = float64(object.Y) / 5 + result.LocationX = float64(x) / 5 + result.LocationY = float64(y) / 5 return result } @@ -96,37 +96,37 @@ func (v *AnimatedEntity) LoadLayer(layer string, fileProvider d2interface.FilePr layerName := "tr" switch strings.ToUpper(layer) { case "HD": // Head - layerName = v.object.Lookup.HD + layerName = v.object.HD case "TR": // Torso - layerName = v.object.Lookup.TR + layerName = v.object.TR case "LG": // Legs - layerName = v.object.Lookup.LG + layerName = v.object.LG case "RA": // RightArm - layerName = v.object.Lookup.RA + layerName = v.object.RA case "LA": // LeftArm - layerName = v.object.Lookup.LA + layerName = v.object.LA case "RH": // RightHand - layerName = v.object.Lookup.RH + layerName = v.object.RH case "LH": // LeftHand - layerName = v.object.Lookup.LH + layerName = v.object.LH case "SH": // Shield - layerName = v.object.Lookup.SH + layerName = v.object.SH case "S1": // Special1 - layerName = v.object.Lookup.S1 + layerName = v.object.S1 case "S2": // Special2 - layerName = v.object.Lookup.S2 + layerName = v.object.S2 case "S3": // Special3 - layerName = v.object.Lookup.S3 + layerName = v.object.S3 case "S4": // Special4 - layerName = v.object.Lookup.S4 + layerName = v.object.S4 case "S5": // Special5 - layerName = v.object.Lookup.S5 + layerName = v.object.S5 case "S6": // Special6 - layerName = v.object.Lookup.S6 + layerName = v.object.S6 case "S7": // Special7 - layerName = v.object.Lookup.S7 + layerName = v.object.S7 case "S8": // Special8 - layerName = v.object.Lookup.S8 + layerName = v.object.S8 } if len(layerName) == 0 { return d2dcc.DCC{} diff --git a/d2render/d2mapengine/engine.go b/d2render/d2mapengine/engine.go index 51001a00..067eced7 100644 --- a/d2render/d2mapengine/engine.go +++ b/d2render/d2mapengine/engine.go @@ -5,6 +5,8 @@ import ( "math/rand" "strings" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2helper" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -40,7 +42,7 @@ func CreateMapEngine(gameState *d2core.GameState, soundManager *d2audio.Manager, return result } -func (v *Engine) GenerateMap(regionType RegionIdType, levelPreset int) { +func (v *Engine) GenerateMap(regionType d2enum.RegionIdType, levelPreset int) { randomSource := rand.NewSource(v.gameState.Seed) region := LoadRegion(randomSource, regionType, levelPreset, v.fileProvider) v.regions = append(v.regions, EngineRegion{ @@ -52,19 +54,19 @@ func (v *Engine) GenerateMap(regionType RegionIdType, levelPreset int) { func (v *Engine) GenerateAct1Overworld() { v.soundManager.PlayBGM("/data/global/music/Act1/town1.wav") // TODO: Temp stuff here randomSource := rand.NewSource(v.gameState.Seed) - region := LoadRegion(randomSource, RegionAct1Town, 1, v.fileProvider) + region := LoadRegion(randomSource, d2enum.RegionAct1Town, 1, v.fileProvider) v.regions = append(v.regions, EngineRegion{ Rect: d2common.Rectangle{0, 0, int(region.TileWidth), int(region.TileHeight)}, Region: region, }) if strings.Contains(region.RegionPath, "E1") { - region2 := LoadRegion(randomSource, RegionAct1Town, 2, v.fileProvider) + region2 := LoadRegion(randomSource, d2enum.RegionAct1Town, 2, v.fileProvider) v.regions = append(v.regions, EngineRegion{ Rect: d2common.Rectangle{int(region.TileWidth - 1), 0, int(region2.TileWidth), int(region2.TileHeight)}, Region: region2, }) } else if strings.Contains(region.RegionPath, "S1") { - region2 := LoadRegion(randomSource, RegionAct1Town, 3, v.fileProvider) + region2 := LoadRegion(randomSource, d2enum.RegionAct1Town, 3, v.fileProvider) v.regions = append(v.regions, EngineRegion{ Rect: d2common.Rectangle{0, int(region.TileHeight - 1), int(region2.TileWidth), int(region2.TileHeight)}, Region: region2, @@ -116,19 +118,19 @@ func (v *Engine) RenderTile(region *Region, offX, offY, x, y int, target *ebiten if tile.Floors[i].Hidden || tile.Floors[i].Prop1 == 0 { continue } - region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, RegionLayerTypeFloors, i, target) + region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, d2enum.RegionLayerTypeFloors, i, target) } for i := range tile.Shadows { if tile.Shadows[i].Hidden || tile.Shadows[i].Prop1 == 0 { continue } - region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, RegionLayerTypeShadows, i, target) + region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, d2enum.RegionLayerTypeShadows, i, target) } for i := range tile.Walls { if tile.Walls[i].Hidden || tile.Walls[i].Orientation == 15 || tile.Walls[i].Orientation == 10 || tile.Walls[i].Orientation == 11 || tile.Walls[i].Orientation == 0 { continue } - region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, RegionLayerTypeWalls, i, target) + region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, d2enum.RegionLayerTypeWalls, i, target) } for _, obj := range region.AnimationEntities { if int(math.Floor(obj.LocationX)) == x && int(math.Floor(obj.LocationY)) == y { @@ -144,6 +146,6 @@ func (v *Engine) RenderTile(region *Region, offX, offY, x, y int, target *ebiten if tile.Walls[i].Hidden || tile.Walls[i].Orientation != 15 { continue } - region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, RegionLayerTypeWalls, i, target) + region.RenderTile(offX+int(v.OffsetX), offY+int(v.OffsetY), x, y, d2enum.RegionLayerTypeWalls, i, target) } } diff --git a/d2render/d2mapengine/region.go b/d2render/d2mapengine/region.go index 826c567e..557165c6 100644 --- a/d2render/d2mapengine/region.go +++ b/d2render/d2mapengine/region.go @@ -29,12 +29,6 @@ import ( "github.com/hajimehoshi/ebiten" ) -type TileCacheRecord struct { - Image *ebiten.Image - XOffset int - YOffset int -} - type Region struct { RegionPath string LevelType d2datadict.LevelTypeRecord @@ -53,55 +47,7 @@ type Region struct { StartY float64 } -type RegionLayerType int - -const ( - RegionLayerTypeFloors RegionLayerType = 0 - RegionLayerTypeWalls RegionLayerType = 1 - RegionLayerTypeShadows RegionLayerType = 2 -) - -type RegionIdType int - -const ( - RegionAct1Town RegionIdType = 1 - RegionAct1Wilderness RegionIdType = 2 - RegionAct1Cave RegionIdType = 3 - RegionAct1Crypt RegionIdType = 4 - RegionAct1Monestary RegionIdType = 5 - RegionAct1Courtyard RegionIdType = 6 - RegionAct1Barracks RegionIdType = 7 - RegionAct1Jail RegionIdType = 8 - RegionAct1Cathedral RegionIdType = 9 - RegionAct1Catacombs RegionIdType = 10 - RegionAct1Tristram RegionIdType = 11 - RegionAct2Town RegionIdType = 12 - RegionAct2Sewer RegionIdType = 13 - RegionAct2Harem RegionIdType = 14 - RegionAct2Basement RegionIdType = 15 - RegionAct2Desert RegionIdType = 16 - RegionAct2Tomb RegionIdType = 17 - RegionAct2Lair RegionIdType = 18 - RegionAct2Arcane RegionIdType = 19 - RegionAct3Town RegionIdType = 20 - RegionAct3Jungle RegionIdType = 21 - RegionAct3Kurast RegionIdType = 22 - RegionAct3Spider RegionIdType = 23 - RegionAct3Dungeon RegionIdType = 24 - RegionAct3Sewer RegionIdType = 25 - RegionAct4Town RegionIdType = 26 - RegionAct4Mesa RegionIdType = 27 - RegionAct4Lava RegionIdType = 28 - RegonAct5Town RegionIdType = 29 - RegionAct5Siege RegionIdType = 30 - RegionAct5Barricade RegionIdType = 31 - RegionAct5Temple RegionIdType = 32 - RegionAct5IceCaves RegionIdType = 33 - RegionAct5Baal RegionIdType = 34 - RegionAct5Lava RegionIdType = 35 -) - -func LoadRegion(seed rand.Source, levelType RegionIdType, levelPreset int, fileProvider d2interface.FileProvider) *Region { +func LoadRegion(seed rand.Source, levelType d2enum.RegionIdType, levelPreset int, fileProvider d2interface.FileProvider) *Region { result := &Region{ LevelType: d2datadict.LevelTypes[levelType], levelPreset: d2datadict.LevelPresets[levelPreset], @@ -111,7 +57,7 @@ func LoadRegion(seed rand.Source, levelType RegionIdType, levelPreset int, fileP WallCache: make(map[uint32]*TileCacheRecord), } result.Palette = d2datadict.Palettes[d2enum.PaletteType("act"+strconv.Itoa(int(result.LevelType.Act)))] - //\bm := result.levelPreset.Dt1Mask + //bm := result.levelPreset.Dt1Mask for _, levelTypeDt1 := range result.LevelType.Files { /* if bm&1 == 0 { @@ -158,13 +104,14 @@ func (v *Region) loadObjects(fileProvider d2interface.FileProvider) { if object.Lookup.Base == "" || object.Lookup.Token == "" || object.Lookup.TR == "" { return } - npc := d2core.CreateNPC(object, fileProvider) + npc := d2core.CreateNPC(object.X, object.Y, object.Lookup, fileProvider, 1) + npc.SetPaths(object.Paths) v.NPCs = append(v.NPCs, npc) case d2datadict.ObjectTypeItem: if object.ObjectInfo == nil || !object.ObjectInfo.Draw || object.Lookup.Base == "" || object.Lookup.Token == "" { return } - entity := d2render.CreateAnimatedEntity(object, fileProvider, d2enum.Units) + entity := d2render.CreateAnimatedEntity(object.X, object.Y, object.Lookup, fileProvider, d2enum.Units) entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0, fileProvider) v.AnimationEntities = append(v.AnimationEntities, entity) } @@ -173,14 +120,14 @@ func (v *Region) loadObjects(fileProvider d2interface.FileProvider) { wg.Wait() } -func (v *Region) RenderTile(offsetX, offsetY, tileX, tileY int, layerType RegionLayerType, layerIndex int, target *ebiten.Image) { +func (v *Region) RenderTile(offsetX, offsetY, tileX, tileY int, layerType d2enum.RegionLayerType, layerIndex int, target *ebiten.Image) { offsetX -= 80 switch layerType { - case RegionLayerTypeFloors: + case d2enum.RegionLayerTypeFloors: v.renderFloor(v.DS1.Tiles[tileY][tileX].Floors[layerIndex], offsetX, offsetY, target) - case RegionLayerTypeWalls: + case d2enum.RegionLayerTypeWalls: v.renderWall(v.DS1.Tiles[tileY][tileX].Walls[layerIndex], offsetX, offsetY, target) - case RegionLayerTypeShadows: + case d2enum.RegionLayerTypeShadows: v.renderShadow(v.DS1.Tiles[tileY][tileX].Shadows[layerIndex], offsetX, offsetY, target) } } @@ -394,7 +341,10 @@ func (v *Region) generateWallCache(tile d2ds1.WallRecord) *TileCacheRecord { yAdjust = int(tileMinY) + 80 } - image.ReplacePixels(pixels) + // TODO: This may also need to be an atlas, but could get pretty large... + if err := image.ReplacePixels(pixels); err != nil { + log.Panicf(err.Error()) + } return &TileCacheRecord{ image, 0, diff --git a/d2render/d2mapengine/tile_cache_record.go b/d2render/d2mapengine/tile_cache_record.go new file mode 100644 index 00000000..2694e359 --- /dev/null +++ b/d2render/d2mapengine/tile_cache_record.go @@ -0,0 +1,9 @@ +package d2mapengine + +import "github.com/hajimehoshi/ebiten" + +type TileCacheRecord struct { + Image *ebiten.Image + XOffset int + YOffset int +} diff --git a/d2render/d2ui/button.go b/d2render/d2ui/button.go index b19a0ac5..da4dd406 100644 --- a/d2render/d2ui/button.go +++ b/d2render/d2ui/button.go @@ -22,12 +22,13 @@ import ( type ButtonType int const ( - ButtonTypeWide ButtonType = 1 - ButtonTypeMedium ButtonType = 2 - ButtonTypeNarrow ButtonType = 3 - ButtonTypeCancel ButtonType = 4 - ButtonTypeTall ButtonType = 5 - ButtonTypeShort ButtonType = 6 + ButtonTypeWide ButtonType = 1 + ButtonTypeMedium ButtonType = 2 + ButtonTypeNarrow ButtonType = 3 + ButtonTypeCancel ButtonType = 4 + ButtonTypeTall ButtonType = 5 + ButtonTypeShort ButtonType = 6 + ButtonTypeOkCancel ButtonType = 7 // Game UI @@ -63,10 +64,11 @@ type ButtonLayout struct { // ButtonLayouts define the type of buttons you can have var ButtonLayouts = map[ButtonType]ButtonLayout{ - ButtonTypeWide: {2, 1, d2resource.WideButtonBlank, d2enum.Units, false, 0, -1, d2resource.FontExocet10, nil, true, 1}, - ButtonTypeShort: {1, 1, d2resource.ShortButtonBlank, d2enum.Units, false, 0, -1, d2resource.FontRediculous, nil, true, -1}, - ButtonTypeMedium: {1, 1, d2resource.MediumButtonBlank, d2enum.Units, false, 0, 0, d2resource.FontExocet10, nil, true, 0}, - ButtonTypeTall: {1, 1, d2resource.TallButtonBlank, d2enum.Units, false, 0, 0, d2resource.FontExocet10, nil, true, 5}, + ButtonTypeWide: {2, 1, d2resource.WideButtonBlank, d2enum.Units, false, 0, -1, d2resource.FontExocet10, nil, true, 1}, + ButtonTypeShort: {1, 1, d2resource.ShortButtonBlank, d2enum.Units, false, 0, -1, d2resource.FontRediculous, nil, true, -1}, + ButtonTypeMedium: {1, 1, d2resource.MediumButtonBlank, d2enum.Units, false, 0, 0, d2resource.FontExocet10, nil, true, 0}, + ButtonTypeTall: {1, 1, d2resource.TallButtonBlank, d2enum.Units, false, 0, 0, d2resource.FontExocet10, nil, true, 5}, + ButtonTypeOkCancel: {1, 1, d2resource.CancelButton, d2enum.Units, false, 0, -1, d2resource.FontRediculous, nil, true, 0}, /* {eButtonType.Wide, new ButtonLayout { XSegments = 2, ResourceName = ResourcePaths.WideButtonBlank, PaletteName = PaletteDefs.Units } }, {eButtonType.Narrow, new ButtonLayout { ResourceName = ResourcePaths.NarrowButtonBlank, PaletteName = PaletteDefs.Units } }, @@ -171,7 +173,7 @@ func (v *Button) OnActivated(callback func()) { } // Activate calls the on activated callback handler, if any -func (v Button) Activate() { +func (v *Button) Activate() { if v.onClick == nil { return } diff --git a/tests/mapload_test.go b/tests/mapload_test.go index fb13e0f3..2795e8c5 100644 --- a/tests/mapload_test.go +++ b/tests/mapload_test.go @@ -17,7 +17,7 @@ func TestMapGenerationPerformance(t *testing.T) { d2common.ConfigBasePath = "../" engine := d2core.CreateEngine() - gameState := d2core.CreateGameState() + gameState := d2core.CreateTestGameState() mapEngine := _map.CreateMapEngine(gameState, engine.SoundManager, engine) mapEngine.GenerateAct1Overworld() surface, _ := ebiten.NewImage(800, 600, ebiten.FilterNearest)