package d2gamescreen import ( "fmt" "log" "os" "strings" "time" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" ) const ( subtilesPerTile = 5 ) type regionSpec struct { regionType d2enum.RegionIdType startPresetIndex int endPresetIndex int extra []int } func getRegions() []regionSpec { return []regionSpec{ // Act I {d2enum.RegionAct1Town, 1, 3, []int{}}, {d2enum.RegionAct1Wilderness, 4, 52, []int{ 108, 160, 161, 162, 163, 164, }}, {d2enum.RegionAct1Cave, 53, 107, []int{}}, {d2enum.RegionAct1Crypt, 109, 159, []int{}}, {d2enum.RegionAct1Monestary, 165, 165, []int{}}, {d2enum.RegionAct1Courtyard, 166, 166, []int{256}}, {d2enum.RegionAct1Barracks, 167, 205, []int{}}, {d2enum.RegionAct1Jail, 206, 255, []int{}}, {d2enum.RegionAct1Cathedral, 257, 257, []int{}}, {d2enum.RegionAct1Catacombs, 258, 299, []int{}}, {d2enum.RegionAct1Tristram, 300, 300, []int{}}, // Act II {d2enum.RegionAct2Town, 301, 301, []int{}}, {d2enum.RegionAct2Sewer, 302, 352, []int{}}, {d2enum.RegionAct2Harem, 353, 357, []int{}}, {d2enum.RegionAct2Basement, 358, 361, []int{}}, {d2enum.RegionAct2Desert, 362, 413, []int{}}, {d2enum.RegionAct2Tomb, 414, 481, []int{}}, {d2enum.RegionAct2Lair, 482, 509, []int{}}, {d2enum.RegionAct2Arcane, 510, 528, []int{}}, // Act III {d2enum.RegionAct3Town, 529, 529, []int{}}, {d2enum.RegionAct3Jungle, 530, 604, []int{}}, {d2enum.RegionAct3Kurast, 605, 658, []int{ 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, }}, {d2enum.RegionAct3Spider, 659, 664, []int{}}, {d2enum.RegionAct3Dungeon, 665, 704, []int{}}, {d2enum.RegionAct3Sewer, 705, 747, []int{}}, // Act IV {d2enum.RegionAct4Town, 797, 798, []int{}}, {d2enum.RegionAct4Mesa, 799, 835, []int{}}, {d2enum.RegionAct4Lava, 836, 862, []int{}}, // Act V -- broken or wrong order {d2enum.RegonAct5Town, 863, 864, []int{}}, {d2enum.RegionAct5Siege, 865, 879, []int{}}, {d2enum.RegionAct5Barricade, 880, 1002, []int{}}, {d2enum.RegionAct5IceCaves, 1003, 1041, []int{}}, {d2enum.RegionAct5Temple, 1042, 1052, []int{}}, {d2enum.RegionAct5Baal, 1059, 1090, []int{}}, {d2enum.RegionAct5Lava, 1053, 1058, []int{}}, } } // MapEngineTest represents the MapEngineTest screen type MapEngineTest struct { asset *d2asset.AssetManager playerStateFactory *d2hero.HeroStateFactory playerState *d2hero.HeroState mapEngine *d2mapengine.MapEngine mapGen *d2mapgen.MapGenerator mapRenderer *d2maprenderer.MapRenderer terminal d2interface.Terminal renderer d2interface.Renderer inputManager d2interface.InputManager audioProvider d2interface.AudioProvider screen *d2screen.ScreenManager lastMouseX, lastMouseY int selX, selY int selectedTile *d2mapengine.MapTile // https://github.com/OpenDiablo2/OpenDiablo2/issues/806 currentRegion int levelPreset int fileIndex int regionSpec regionSpec filesCount int } // CreateMapEngineTest creates the Map Engine Test screen and returns a pointer to it func CreateMapEngineTest(currentRegion, levelPreset int, asset *d2asset.AssetManager, term d2interface.Terminal, renderer d2interface.Renderer, inputManager d2interface.InputManager, audioProvider d2interface.AudioProvider, screen *d2screen.ScreenManager, ) (*MapEngineTest, error) { heroStateFactory, err := d2hero.NewHeroStateFactory(asset) if err != nil { return nil, err } result := &MapEngineTest{ currentRegion: currentRegion, levelPreset: levelPreset, fileIndex: 0, regionSpec: regionSpec{}, filesCount: 0, asset: asset, terminal: term, renderer: renderer, inputManager: inputManager, audioProvider: audioProvider, screen: screen, playerStateFactory: heroStateFactory, } result.playerState = heroStateFactory.CreateTestGameState() return result, nil } func (met *MapEngineTest) loadRegionByIndex(n, levelPreset, fileIndex int) { log.Printf("Loaded region: Type(%d) LevelPreset(%d) FileIndex(%d)", n, levelPreset, fileIndex) met.mapRenderer.InvalidateImageCache() for _, spec := range getRegions() { if spec.regionType != d2enum.RegionIdType(n) { continue } met.regionSpec = spec inExtra := false for _, e := range spec.extra { if e == levelPreset { inExtra = true break } } if !inExtra { if levelPreset < spec.startPresetIndex { levelPreset = spec.startPresetIndex } if levelPreset > spec.endPresetIndex { levelPreset = spec.endPresetIndex } } met.levelPreset = levelPreset } mapGen, _ := d2mapgen.NewMapGenerator(met.asset, met.mapEngine) met.mapGen = mapGen if n == 0 { met.mapEngine.SetSeed(time.Now().UnixNano()) met.mapGen.GenerateAct1Overworld() } else { met.mapEngine = d2mapengine.CreateMapEngine(met.asset) // necessary for map name update met.mapEngine.SetSeed(time.Now().UnixNano()) met.mapEngine.GenerateMap(d2enum.RegionIdType(n), levelPreset, fileIndex) } met.mapRenderer.SetMapEngine(met.mapEngine) position := d2vector.NewPosition(met.mapRenderer.WorldToOrtho(met.mapEngine.GetCenterPosition())) met.mapRenderer.SetCameraPosition(&position) musicDef := d2resource.GetMusicDef(met.regionSpec.regionType) met.audioProvider.PlayBGM(musicDef.MusicFile) } // OnLoad loads the resources for the Map Engine Test screen func (met *MapEngineTest) OnLoad(loading d2screen.LoadingState) { if err := met.inputManager.BindHandler(met); err != nil { fmt.Printf("could not add MapEngineTest as event handler") } loading.Progress(twentyPercent) met.mapEngine = d2mapengine.CreateMapEngine(met.asset) loading.Progress(fiftyPercent) met.mapRenderer = d2maprenderer.CreateMapRenderer(met.asset, met.renderer, met.mapEngine, met.terminal, 0.0, 0.0) loading.Progress(seventyPercent) met.loadRegionByIndex(met.currentRegion, met.levelPreset, met.fileIndex) } // OnUnload releases the resources for the Map Engine Test screen func (met *MapEngineTest) OnUnload() error { // https://github.com/OpenDiablo2/OpenDiablo2/issues/792 if err := met.inputManager.UnbindHandler(met); err != nil { return err } return nil } const ( lineSmallOffsetY = 12 lineNormalOffsetY = 16 lineSmallIndentX = 10 lineNormalIndentX = 15 lineBigIndentX = 170 // distance between text columns ) // Render renders the Map Engine Test screen func (met *MapEngineTest) Render(screen d2interface.Surface) error { met.mapRenderer.Render(screen) screen.PushTranslation(0, lineNormalOffsetY) defer screen.Pop() screen.DrawTextf("N - next region, P - previous region") screen.PushTranslation(0, lineNormalOffsetY) defer screen.Pop() screen.DrawTextf("Shift+N - next preset, Shift+P - previous preset") screen.PushTranslation(0, lineNormalOffsetY) defer screen.Pop() screen.DrawTextf("Ctrl+N - next file, Ctrl+P - previous file") screen.PushTranslation(0, lineNormalOffsetY) defer screen.Pop() screen.DrawTextf("Left click selects tile, right click unselects") screen.PushTranslation(0, lineNormalOffsetY) defer screen.Pop() if met.selectedTile == nil { screen.PushTranslation(lineNormalIndentX, lineNormalOffsetY) defer screen.Pop() screen.DrawTextf("No tile selected") } else { screen.PushTranslation(lineSmallIndentX, lineNormalOffsetY) defer screen.Pop() screen.PushTranslation(0, lineNormalOffsetY) // extra vspace defer screen.Pop() screen.DrawTextf("Tile %v,%v", met.selX, met.selY) screen.PushTranslation(lineNormalIndentX, lineNormalOffsetY) defer screen.Pop() screen.DrawTextf("Walls") tpop := 0 for _, wall := range met.selectedTile.Components.Walls { screen.PushTranslation(0, lineSmallOffsetY) tpop++ tmpString := fmt.Sprintf("%#v", wall) stringSlice := strings.Split(tmpString, " ") tmp2 := strings.Split(stringSlice[0], "{") stringSlice[0] = tmp2[1] for _, str := range stringSlice { screen.PushTranslation(0, lineSmallOffsetY) tpop++ screen.DrawTextf(str) } } screen.PopN(tpop) screen.PushTranslation(lineBigIndentX, 0) defer screen.Pop() screen.DrawTextf("Floors") tpop = 0 for _, floor := range met.selectedTile.Components.Floors { screen.PushTranslation(0, lineSmallOffsetY) tpop++ tmpString := fmt.Sprintf("%#v", floor) stringSlice := strings.Split(tmpString, " ") tmp2 := strings.Split(stringSlice[0], "{") stringSlice[0] = tmp2[1] for _, str := range stringSlice { screen.PushTranslation(0, lineSmallOffsetY) tpop++ screen.DrawTextf(str) } } screen.PopN(tpop) screen.PushTranslation(lineBigIndentX, 0) defer screen.Pop() screen.DrawTextf("Shadows") tpop = 0 for _, shadow := range met.selectedTile.Components.Shadows { screen.PushTranslation(0, lineSmallOffsetY) tpop++ tmpString := fmt.Sprintf("%#v", shadow) stringSlice := strings.Split(tmpString, " ") tmp2 := strings.Split(stringSlice[0], "{") stringSlice[0] = tmp2[1] for _, str := range stringSlice { screen.PushTranslation(0, lineSmallOffsetY) tpop++ screen.DrawTextf(str) } } screen.PopN(tpop) screen.PushTranslation(lineBigIndentX, 0) defer screen.Pop() screen.DrawTextf("Substitutions") tpop = 0 for _, subst := range met.selectedTile.Components.Substitutions { screen.PushTranslation(0, lineSmallOffsetY) tpop++ tmpString := fmt.Sprintf("%#v", subst) stringSlice := strings.Split(tmpString, " ") tmp2 := strings.Split(stringSlice[0], "{") stringSlice[0] = tmp2[1] for _, str := range stringSlice { screen.PushTranslation(0, lineSmallOffsetY) tpop++ screen.DrawTextf(str) } } screen.PopN(tpop) } return nil } // OnMouseMove is the mouse move handler func (met *MapEngineTest) OnMouseMove(event d2interface.MouseMoveEvent) bool { mx, my := event.X(), event.Y() met.lastMouseX = mx met.lastMouseY = my return false } // OnMouseButtonDown handles mouse button down events func (met *MapEngineTest) OnMouseButtonDown(event d2interface.MouseEvent) bool { if event.Button() == d2enum.MouseButtonLeft { met.handleLeftClick() return true } if event.Button() == d2enum.MouseButtonRight { met.selectedTile = nil return true } return false } // OnMouseButtonRepeat handles repeated mouse clicks func (met *MapEngineTest) OnMouseButtonRepeat(event d2interface.MouseEvent) bool { if event.Button() == d2enum.MouseButtonLeft { met.handleLeftClick() return true } return false } func (met *MapEngineTest) handleLeftClick() { px, py := met.mapRenderer.ScreenToWorld(met.lastMouseX, met.lastMouseY) met.selX = int(px) met.selY = int(py) met.selectedTile = met.mapEngine.TileAt(int(px), int(py)) camVect := met.mapRenderer.Camera.GetPosition().Vector halfScreenWidth, halfScreenHeight := screenWidth>>1, screenHeight>>1 x := float64(met.lastMouseX-halfScreenWidth) / subtilesPerTile y := float64(met.lastMouseY-halfScreenHeight) / subtilesPerTile targetPosition := d2vector.NewPositionTile(x, y) targetPosition.Add(&camVect) met.mapRenderer.SetCameraTarget(&targetPosition) } // Advance runs the update logic on the Map Engine Test screen func (met *MapEngineTest) Advance(tickTime float64) error { met.mapEngine.Advance(tickTime) met.mapRenderer.Advance(tickTime) return nil } // OnKeyRepeat is called to handle repeated key presses func (met *MapEngineTest) OnKeyRepeat(event d2interface.KeyEvent) bool { var moveSpeed float64 = 8 if event.KeyMod() == d2enum.KeyModShift { moveSpeed *= 2 } if event.Key() == d2enum.KeyDown { v := d2vector.NewVector(0, moveSpeed) met.mapRenderer.MoveCameraTargetBy(v) return true } if event.Key() == d2enum.KeyUp { v := d2vector.NewVector(0, -moveSpeed) met.mapRenderer.MoveCameraTargetBy(v) return true } if event.Key() == d2enum.KeyRight { v := d2vector.NewVector(moveSpeed, 0) met.mapRenderer.MoveCameraTargetBy(v) return true } if event.Key() == d2enum.KeyLeft { v := d2vector.NewVector(-moveSpeed, 0) met.mapRenderer.MoveCameraTargetBy(v) return true } return false } // OnKeyDown defines the actions of the Map Engine Test screen when a key is pressed func (met *MapEngineTest) OnKeyDown(event d2interface.KeyEvent) bool { if event.Key() == d2enum.KeyEscape { os.Exit(0) return true } if event.Key() == d2enum.KeyN { switch event.KeyMod() { case d2enum.KeyModControl: met.fileIndex++ met.screen.SetNextScreen(met) case d2enum.KeyModShift: met.levelPreset = increment(met.levelPreset, met.regionSpec.startPresetIndex, met.regionSpec.endPresetIndex) met.screen.SetNextScreen(met) default: met.currentRegion = increment(met.currentRegion, 0, len(getRegions())) met.screen.SetNextScreen(met) } return true } if event.Key() == d2enum.KeyP { switch event.KeyMod() { case d2enum.KeyModControl: met.fileIndex-- met.screen.SetNextScreen(met) case d2enum.KeyModShift: met.levelPreset = decrement(met.levelPreset, met.regionSpec.startPresetIndex, met.regionSpec.endPresetIndex) met.screen.SetNextScreen(met) default: met.currentRegion = decrement(met.currentRegion, 0, len(getRegions())) met.screen.SetNextScreen(met) } return true } return false } func increment(v, min, max int) int { v++ if v > max { return min } return v } func decrement(v, min, max int) int { v-- if v < min { return max } return v }