diff --git a/d2common/d2data/d2datadict/unique_items.go b/d2common/d2data/d2datadict/unique_items.go index 0adefd2f..ebc49ae0 100644 --- a/d2common/d2data/d2datadict/unique_items.go +++ b/d2common/d2data/d2datadict/unique_items.go @@ -135,7 +135,7 @@ func LoadUniqueItems(file []byte) { } rec := createUniqueItemRecord(r) - UniqueItems[rec.Code] = &rec + UniqueItems[rec.Name] = &rec } log.Printf("Loaded %d unique items", len(UniqueItems)) diff --git a/d2common/d2interface/map_entity.go b/d2common/d2interface/map_entity.go index b5a09f7c..918a6744 100644 --- a/d2common/d2interface/map_entity.go +++ b/d2common/d2interface/map_entity.go @@ -11,7 +11,7 @@ type MapEntity interface { GetSize() (width, height int) GetLayer() int GetPositionF() (float64, float64) - Name() string + Label() string Selectable() bool Highlight() } diff --git a/d2core/d2item/diablo2item/item.go b/d2core/d2item/diablo2item/item.go index ad7511e8..d4b45f3d 100644 --- a/d2core/d2item/diablo2item/item.go +++ b/d2core/d2item/diablo2item/item.go @@ -31,7 +31,6 @@ const ( propertyIndestructable = "indestruct" ) - const ( magicItemPrefixMax = 1 magicItemSuffixMax = 1 @@ -95,6 +94,8 @@ type itemAttributes struct { requiredDexterity int classSpecific d2enum.Hero + identitified bool + crafted bool durable bool // some items specify that they have no durability indestructable bool ethereal bool @@ -107,9 +108,39 @@ type minMaxEnhanceable struct { enhance int } -// Name returns the item name -func (i *Item) Name() string { - return i.name +// Label returns the item name +func (i *Item) Label() string { + str := i.name + + if i.attributes.crafted { + return d2ui.ColorTokenize(str, d2ui.ColorTokenCraftedItem) + } + + if i.SetItemRecord() != nil { + return d2ui.ColorTokenize(str, d2ui.ColorTokenSetItem) + } + + if i.UniqueRecord() != nil { + return d2ui.ColorTokenize(str, d2ui.ColorTokenUniqueItem) + } + + numAffixes := len(i.PrefixRecords()) + len(i.SuffixRecords()) + + if numAffixes > 0 && numAffixes < 3 { + return d2ui.ColorTokenize(str, d2ui.ColorTokenMagicItem) + } + + if numAffixes > 2 { + return d2ui.ColorTokenize(str, d2ui.ColorTokenRareItem) + } + + if i.sockets != nil { + if len(i.sockets) > 0 { + return d2ui.ColorTokenize(str, d2ui.ColorTokenSocketedItem) + } + } + + return d2ui.ColorTokenize(str, d2ui.ColorTokenNormalItem) } // Context returns the statContext that is being used to evaluate stats. for example, @@ -188,7 +219,6 @@ func affixRecords( return result } - // SlotType returns the slot type (where it can be equipped) func (i *Item) SlotType() d2enum.EquippedSlot { return i.slotType @@ -358,7 +388,7 @@ func (i *Item) init() *Item { if i.rand == nil { i.SetSeed(0) } - + i.generateAllProperties() i.updateItemAttributes() return i @@ -471,7 +501,7 @@ func (i *Item) updateItemAttributes() { } def, minDef, maxDef := 0, r.MinAC, r.MaxAC - + if maxDef < minDef { minDef, maxDef = maxDef, minDef } diff --git a/d2core/d2item/item.go b/d2core/d2item/item.go index 0198a3d7..f54b7cbf 100644 --- a/d2core/d2item/item.go +++ b/d2core/d2item/item.go @@ -6,6 +6,6 @@ type Item interface { Context() StatContext SetContext(StatContext) - Name() string + Label() string Description() string } diff --git a/d2core/d2map/d2mapentity/item.go b/d2core/d2map/d2mapentity/item.go index 65de7bd6..86c42a4e 100644 --- a/d2core/d2map/d2mapentity/item.go +++ b/d2core/d2map/d2mapentity/item.go @@ -1,10 +1,14 @@ package d2mapentity import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2item/diablo2item" ) +// static check that item implements map entity interface +var _ d2interface.MapEntity = &Item{} + const ( errInvalidItemCodes = "invalid item codes supplied" ) @@ -36,8 +40,8 @@ func (i *Item) Highlight() { } // Name returns the item name -func (i *Item) Name() string { - return i.Item.Name() +func (i *Item) Label() string { + return i.Item.Label() } // GetSize returns the current frame size diff --git a/d2core/d2map/d2mapentity/map_entity.go b/d2core/d2map/d2mapentity/map_entity.go index 149f1dd0..9a362d8a 100644 --- a/d2core/d2map/d2mapentity/map_entity.go +++ b/d2core/d2map/d2mapentity/map_entity.go @@ -196,8 +196,8 @@ func (m *mapEntity) GetPositionF() (x, y float64) { return w.X(), w.Y() } -// Name returns the NPC's in-game name (e.g. "Deckard Cain") or an empty string if it does not have a name -func (m *mapEntity) Name() string { +// Label returns the NPC's in-game name (e.g. "Deckard Cain") or an empty string if it does not have a name +func (m *mapEntity) Label() string { return "" } diff --git a/d2core/d2map/d2mapentity/npc.go b/d2core/d2map/d2mapentity/npc.go index fb96a3c9..056ee13d 100644 --- a/d2core/d2map/d2mapentity/npc.go +++ b/d2core/d2map/d2mapentity/npc.go @@ -161,8 +161,8 @@ func (v *NPC) Selectable() bool { return v.name != "" } -// Name returns the NPC's in-game name (e.g. "Deckard Cain") or an empty string if it does not have a name. -func (v *NPC) Name() string { +// Label returns the NPC's in-game name (e.g. "Deckard Cain") or an empty string if it does not have a name. +func (v *NPC) Label() string { return v.name } diff --git a/d2core/d2object/object.go b/d2core/d2object/object.go index 353e8515..16c2ca1a 100644 --- a/d2core/d2object/object.go +++ b/d2core/d2object/object.go @@ -128,8 +128,8 @@ func (ob *Object) GetPositionF() (x, y float64) { return w.X(), w.Y() } -// Name gets the name of the object -func (ob *Object) Name() string { +// Label gets the name of the object +func (ob *Object) Label() string { return ob.name } diff --git a/d2core/d2ui/button.go b/d2core/d2ui/button.go index 5ed35cc2..d957dd8a 100644 --- a/d2core/d2ui/button.go +++ b/d2core/d2ui/button.go @@ -112,8 +112,10 @@ func CreateButton(renderer d2interface.Renderer, buttonType ButtonType, text str buttonLayout := getButtonLayouts()[buttonType] result.buttonLayout = buttonLayout lbl := CreateLabel(buttonLayout.FontPath, d2resource.PaletteUnits) + + lbl.SetText(text) - lbl.Color = color.RGBA{R: 100, G: 100, B: 100, A: 255} + lbl.Color[0] = color.RGBA{R: 100, G: 100, B: 100, A: 255} lbl.Alignment = d2gui.HorizontalAlignCenter animation, _ := d2asset.LoadAnimation(buttonLayout.ResourceName, buttonLayout.PaletteName) diff --git a/d2core/d2ui/label.go b/d2core/d2ui/label.go index 46cc82d0..14c6a0d1 100644 --- a/d2core/d2ui/label.go +++ b/d2core/d2ui/label.go @@ -1,15 +1,64 @@ package d2ui import ( + "fmt" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "image/color" "log" + "regexp" "strings" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" ) +// ColorToken is a string which is used inside of label strings to set font color. +type ColorToken string + +const ( + colorTokenFmt = `%s%s` + colorTokenMatch = `\[[^\]]+\]` + colorStrMatch = colorTokenMatch + `[^\[]+` +) + +const ( + ColorTokenGrey ColorToken = "[grey]" + ColorTokenRed ColorToken = "[red]" + ColorTokenWhite ColorToken = "[white]" + ColorTokenBlue ColorToken = "[blue]" + ColorTokenYellow ColorToken = "[yellow]" + ColorTokenGreen ColorToken = "[green]" + ColorTokenGold ColorToken = "[gold]" + ColorTokenOrange ColorToken = "[orange]" + ColorTokenBlack ColorToken = "[black]" +) + +// Color tokens for item labels +const ( + ColorTokenSocketedItem = ColorTokenGrey + ColorTokenNormalItem = ColorTokenWhite + ColorTokenMagicItem = ColorTokenBlue + ColorTokenRareItem = ColorTokenYellow + ColorTokenSetItem = ColorTokenGreen + ColorTokenUniqueItem = ColorTokenGold + ColorTokenCraftedItem = ColorTokenOrange +) + +const ( + ColorTokenServer = ColorTokenRed + ColorTokenButton = ColorTokenBlack +) + +const ( + ColorTokenCharacterName = ColorTokenGold + ColorTokenCharacterDesc = ColorTokenWhite + ColorTokenCharacterType = ColorTokenGreen +) + +// ColorTokenize formats the string with the given color token +func ColorTokenize(s string, t ColorToken) string { + return fmt.Sprintf(colorTokenFmt, t, s) +} + // Label represents a user interface label type Label struct { text string @@ -17,7 +66,7 @@ type Label struct { Y int Alignment d2gui.HorizontalAlign font d2interface.Font - Color color.Color + Color map[int]color.Color } // CreateLabel creates a new instance of a UI label @@ -25,26 +74,44 @@ func CreateLabel(fontPath, palettePath string) Label { font, _ := d2asset.LoadFont(fontPath+".tbl", fontPath+".dc6", palettePath) result := Label{ Alignment: d2gui.HorizontalAlignLeft, - Color: color.White, + Color: map[int]color.Color{0: color.White}, font: font, } return result } -// Render draws the label on the screen, respliting the lines to allow for other alignments +// Render draws the label on the screen, respliting the lines to allow for other alignments. func (v *Label) Render(target d2interface.Surface) { - v.font.SetColor(v.Color) target.PushTranslation(v.X, v.Y) lines := strings.Split(v.text, "\n") yOffset := 0 + lastColor := v.Color[0] + v.font.SetColor(lastColor) + for _, line := range lines { lw, lh := v.GetTextMetrics(line) + characters := []rune(line) + target.PushTranslation(v.getAlignOffset(lw), yOffset) - _ = v.font.RenderText(line, target) + for idx := range characters { + character := string(characters[idx]) + charWidth, _ := v.GetTextMetrics(character) + + if v.Color[idx] != nil { + lastColor = v.Color[idx] + v.font.SetColor(lastColor) + } + + _ = v.font.RenderText(character, target) + + target.PushTranslation(charWidth, 0) + } + + target.PopN(len(characters)) yOffset += lh @@ -72,7 +139,43 @@ func (v *Label) GetTextMetrics(text string) (width, height int) { // SetText sets the label's text func (v *Label) SetText(newText string) { - v.text = newText + v.text = v.processColorTokens(newText) +} + +func (v *Label) processColorTokens(str string) string { + tokenMatch := regexp.MustCompile(colorTokenMatch) + tokenStrMatch := regexp.MustCompile(colorStrMatch) + empty := []byte("") + + tokenPosition := 0 + + withoutTokens := string(tokenMatch.ReplaceAll([]byte(str), empty)) // remove tokens from string + + matches := tokenStrMatch.FindAll([]byte(str), -1) + + if len(matches) == 0 { + v.Color[0] = getColor(ColorTokenWhite) + } + + // we find the index of each token and update the color map. + // the key in the map is the starting index of each color token, the value is the color + for idx := range matches { + match := matches[idx] + matchToken := tokenMatch.Find(match) + matchStr := string(tokenMatch.ReplaceAll(match, empty)) + token := ColorToken(matchToken) + theColor := getColor(token) + + if v.Color == nil { + v.Color = make(map[int]color.Color) + } + + v.Color[tokenPosition] = theColor + + tokenPosition += len(matchStr) + } + + return withoutTokens } func (v *Label) getAlignOffset(textWidth int) int { @@ -88,3 +191,28 @@ func (v *Label) getAlignOffset(textWidth int) int { return 0 } } + +func getColor(token ColorToken) color.Color { + alpha := uint8(255) + + // todo this should really come from the PL2 files + colors := map[ColorToken]color.Color{ + ColorTokenGrey: color.RGBA{105, 105, 105, alpha}, + ColorTokenWhite: color.RGBA{255, 255, 255, alpha}, + ColorTokenBlue: color.RGBA{105, 105, 255, alpha}, + ColorTokenYellow: color.RGBA{255, 255, 100, alpha}, + ColorTokenGreen: color.RGBA{0, 255, 0, alpha}, + ColorTokenGold: color.RGBA{199, 179, 119, alpha}, + ColorTokenOrange: color.RGBA{255, 168, 0, alpha}, + ColorTokenRed: color.RGBA{255, 77, 77, alpha}, + ColorTokenBlack: color.RGBA{0, 0, 0, alpha}, + } + + chosen := colors[token] + + if chosen == nil { + return colors[ColorTokenWhite] + } + + return chosen +} diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index 429c7872..e2a431c1 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -175,8 +175,8 @@ func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { } v.characterNameLabel[i] = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) - v.characterNameLabel[i].Color = rgbaColor(lightBrown) v.characterNameLabel[i].SetPosition(offsetX, offsetY) + v.characterNameLabel[i].Color[0] = rgbaColor(lightBrown) offsetY += labelHeight v.characterStatsLabel[i] = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) @@ -184,8 +184,8 @@ func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { offsetY += labelHeight v.characterExpLabel[i] = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteStatic) - v.characterExpLabel[i].Color = rgbaColor(lightGreen) v.characterExpLabel[i].SetPosition(offsetX, offsetY) + v.characterExpLabel[i].Color[0] = rgbaColor(lightGreen) } v.refreshGameStates() } @@ -289,9 +289,12 @@ func (v *CharacterSelect) updateCharacterBoxes() { 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) + heroName := v.gameStates[idx].HeroName + heroInfo := "Level 1 " + v.gameStates[idx].HeroType.String() + + v.characterNameLabel[i].SetText(d2ui.ColorTokenize(heroName, d2ui.ColorTokenGold)) + v.characterStatsLabel[i].SetText(d2ui.ColorTokenize(heroInfo, d2ui.ColorTokenWhite)) + v.characterExpLabel[i].SetText(d2ui.ColorTokenize(expText, d2ui.ColorTokenGreen)) heroType := v.gameStates[idx].HeroType equipment := d2inventory.HeroObjects[heroType] diff --git a/d2game/d2gamescreen/credits.go b/d2game/d2gamescreen/credits.go index 3cd6d842..8ce6b20b 100644 --- a/d2game/d2gamescreen/credits.go +++ b/d2game/d2gamescreen/credits.go @@ -258,9 +258,9 @@ func (v *Credits) getNewFontLabel(isHeading bool) *d2ui.Label { if label.Available { label.Available = false if isHeading { - label.Label.Color = rgbaColor(lightRed) + label.Label.Color[0] = rgbaColor(lightRed) } else { - label.Label.Color = rgbaColor(beige) + label.Label.Color[0] = rgbaColor(beige) } return &label.Label @@ -274,9 +274,9 @@ func (v *Credits) getNewFontLabel(isHeading bool) *d2ui.Label { } if isHeading { - newLabelItem.Label.Color = rgbaColor(lightRed) + newLabelItem.Label.Color[0] = rgbaColor(lightRed) } else { - newLabelItem.Label.Color = rgbaColor(beige) + newLabelItem.Label.Color[0] = rgbaColor(beige) } v.labels = append(v.labels, newLabelItem) diff --git a/d2game/d2gamescreen/main_menu.go b/d2game/d2gamescreen/main_menu.go index c11fb984..2dfed1bd 100644 --- a/d2game/d2gamescreen/main_menu.go +++ b/d2game/d2gamescreen/main_menu.go @@ -188,32 +188,32 @@ func (v *MainMenu) createLabels(loading d2screen.LoadingState) { v.versionLabel = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.versionLabel.Alignment = d2gui.HorizontalAlignRight v.versionLabel.SetText("OpenDiablo2 - " + v.buildInfo.Branch) - v.versionLabel.Color = rgbaColor(white) + v.versionLabel.Color[0] = rgbaColor(white) v.versionLabel.SetPosition(versionLabelX, versionLabelY) v.commitLabel = d2ui.CreateLabel(d2resource.FontFormal10, d2resource.PaletteStatic) v.commitLabel.Alignment = d2gui.HorizontalAlignLeft v.commitLabel.SetText(v.buildInfo.Commit) - v.commitLabel.Color = rgbaColor(white) + v.commitLabel.Color[0] = rgbaColor(white) v.commitLabel.SetPosition(commitLabelX, commitLabelY) v.copyrightLabel = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.copyrightLabel.Alignment = d2gui.HorizontalAlignCenter v.copyrightLabel.SetText("Diablo 2 is © Copyright 2000-2016 Blizzard Entertainment") - v.copyrightLabel.Color = rgbaColor(lightBrown) + v.copyrightLabel.Color[0] = rgbaColor(lightBrown) v.copyrightLabel.SetPosition(copyrightX, copyrightY) loading.Progress(thirtyPercent) v.copyrightLabel2 = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.copyrightLabel2.Alignment = d2gui.HorizontalAlignCenter v.copyrightLabel2.SetText("All Rights Reserved.") - v.copyrightLabel2.Color = rgbaColor(lightBrown) + v.copyrightLabel2.Color[0] = rgbaColor(lightBrown) v.copyrightLabel2.SetPosition(copyright2X, copyright2Y) v.openDiabloLabel = d2ui.CreateLabel(d2resource.FontFormal10, d2resource.PaletteStatic) v.openDiabloLabel.Alignment = d2gui.HorizontalAlignCenter v.openDiabloLabel.SetText("OpenDiablo2 is neither developed by, nor endorsed by Blizzard or its parent company Activision") - v.openDiabloLabel.Color = rgbaColor(lightYellow) + v.openDiabloLabel.Color[0] = rgbaColor(lightYellow) v.openDiabloLabel.SetPosition(od2LabelX, od2LabelY) loading.Progress(fiftyPercent) @@ -226,7 +226,7 @@ func (v *MainMenu) createLabels(loading d2screen.LoadingState) { v.tcpJoinGameLabel.Alignment = d2gui.HorizontalAlignCenter v.tcpJoinGameLabel.SetText("Enter Host IP Address\nto Join Game") - v.tcpJoinGameLabel.Color = rgbaColor(gold) + v.tcpJoinGameLabel.Color[0] = rgbaColor(gold) v.tcpJoinGameLabel.SetPosition(joinGameX, joinGameY) } diff --git a/d2game/d2gamescreen/select_hero_class.go b/d2game/d2gamescreen/select_hero_class.go index 35e94192..160a8f16 100644 --- a/d2game/d2gamescreen/select_hero_class.go +++ b/d2game/d2gamescreen/select_hero_class.go @@ -397,20 +397,17 @@ func (v *SelectHeroClass) createLabels() { v.heroNameLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) v.heroNameLabel.Alignment = d2gui.HorizontalAlignLeft - v.heroNameLabel.Color = rgbaColor(gold) - v.heroNameLabel.SetText("Character Name") + v.heroNameLabel.SetText(d2ui.ColorTokenize("Character Name", d2ui.ColorTokenGold)) v.heroNameLabel.SetPosition(heroNameLabelX, heroNameLabelY) v.expansionCharLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) v.expansionCharLabel.Alignment = d2gui.HorizontalAlignLeft - v.expansionCharLabel.Color = rgbaColor(gold) - v.expansionCharLabel.SetText("EXPANSION CHARACTER") + v.expansionCharLabel.SetText(d2ui.ColorTokenize("EXPANSION CHARACTER", d2ui.ColorTokenGold)) v.expansionCharLabel.SetPosition(expansionLabelX, expansionLabelY) v.hardcoreCharLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) v.hardcoreCharLabel.Alignment = d2gui.HorizontalAlignLeft - v.hardcoreCharLabel.Color = rgbaColor(gold) - v.hardcoreCharLabel.SetText("Hardcore") + v.hardcoreCharLabel.SetText(d2ui.ColorTokenize("Hardcore", d2ui.ColorTokenGold)) v.hardcoreCharLabel.SetPosition(hardcoreLabelX, hardcoreLabelY) } diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index ee19de34..b23ea406 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -94,13 +94,11 @@ func NewGameControls(renderer d2interface.Renderer, hero *d2mapentity.Player, ma }) zoneLabel := d2ui.CreateLabel(d2resource.Font30, d2resource.PaletteUnits) - zoneLabel.Color = color.RGBA{R: 255, G: 88, B: 82, A: 255} zoneLabel.Alignment = d2gui.HorizontalAlignCenter nameLabel := d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic) nameLabel.Alignment = d2gui.HorizontalAlignCenter - nameLabel.SetText("") - nameLabel.Color = color.White + nameLabel.SetText(d2ui.ColorTokenize("", d2ui.ColorTokenServer)) // TODO make this depend on the hero type to respect inventory.txt var inventoryRecordKey string @@ -440,7 +438,8 @@ func (g *GameControls) Render(target d2interface.Surface) error { if within { xOff, yOff := int(entOffset.X()), int(entOffset.Y()) - g.nameLabel.SetText(entity.Name()) + g.nameLabel.SetText(entity.Label()) + xLabel, yLabel := entScreenX-xOff, entScreenY-yOff-entityHeight-hoverLabelOuterPad g.nameLabel.SetPosition(xLabel, yLabel) g.nameLabel.Render(target)