mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-27 21:56:19 -04:00
88326b5278
* Casting a skill now plays the corresponding overlay(if any). * Prevent a crash caused by nil pointer in HeroSkill deserialization, happening when unmarshalling HeroSkill from packets as a remote client. * Add PlayerAnimationModeNone to handle some of the Skills(e.g. Paladin auras) having "" as animation mode. * Joining a game as remote client now waits for map generation to finish before rendering map or processing map entities. This is temporary hack to prevent the game from crashing due to concurrent map read & write exception. * Send CastSkill packet to other clients. Co-authored-by: Presiyan Ivanov <presiyan-ivanov@users.noreply.github.com>
290 lines
8.3 KiB
Go
290 lines
8.3 KiB
Go
package d2mapentity
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
|
|
"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/d2hero"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2item/diablo2item"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
|
|
)
|
|
|
|
// NewMapEntityFactory creates a MapEntityFactory instance with the given asset manager
|
|
func NewMapEntityFactory(asset *d2asset.AssetManager) (*MapEntityFactory, error) {
|
|
itemFactory, err := diablo2item.NewItemFactory(asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stateFactory, err := d2hero.NewHeroStateFactory(asset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entityFactory := &MapEntityFactory{
|
|
stateFactory,
|
|
asset,
|
|
itemFactory,
|
|
}
|
|
|
|
return entityFactory, nil
|
|
}
|
|
|
|
// MapEntityFactory creates map entities for the MapEngine
|
|
type MapEntityFactory struct {
|
|
*d2hero.HeroStateFactory
|
|
asset *d2asset.AssetManager
|
|
item *diablo2item.ItemFactory
|
|
}
|
|
|
|
// NewAnimatedEntity creates an instance of AnimatedEntity
|
|
func NewAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEntity {
|
|
entity := &AnimatedEntity{
|
|
mapEntity: newMapEntity(x, y),
|
|
animation: animation,
|
|
}
|
|
entity.mapEntity.directioner = entity.rotate
|
|
|
|
return entity
|
|
}
|
|
|
|
// NewPlayer creates a new player entity and returns a pointer to it.
|
|
func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero,
|
|
stats *d2hero.HeroStatsState, skills map[int]*d2hero.HeroSkill, equipment *d2inventory.CharacterEquipment) *Player {
|
|
layerEquipment := &[d2enum.CompositeTypeMax]string{
|
|
d2enum.CompositeTypeHead: equipment.Head.GetArmorClass(),
|
|
d2enum.CompositeTypeTorso: equipment.Torso.GetArmorClass(),
|
|
d2enum.CompositeTypeLegs: equipment.Legs.GetArmorClass(),
|
|
d2enum.CompositeTypeRightArm: equipment.RightArm.GetArmorClass(),
|
|
d2enum.CompositeTypeLeftArm: equipment.LeftArm.GetArmorClass(),
|
|
d2enum.CompositeTypeRightHand: equipment.RightHand.GetItemCode(),
|
|
d2enum.CompositeTypeLeftHand: equipment.LeftHand.GetItemCode(),
|
|
d2enum.CompositeTypeShield: equipment.Shield.GetItemCode(),
|
|
}
|
|
|
|
composite, err := f.asset.LoadComposite(d2enum.ObjectTypePlayer, heroType.GetToken(),
|
|
d2resource.PaletteUnits)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
stats.NextLevelExp = f.asset.Records.GetExperienceBreakpoint(heroType, stats.Level)
|
|
stats.Stamina = stats.MaxStamina
|
|
|
|
defaultCharStats := f.asset.Records.Character.Stats[heroType]
|
|
statsState := f.HeroStateFactory.CreateHeroStatsState(heroType, defaultCharStats)
|
|
heroState, _ := f.CreateHeroState(name, heroType, statsState)
|
|
|
|
attackSkillID := 0
|
|
result := &Player{
|
|
mapEntity: newMapEntity(x, y),
|
|
composite: composite,
|
|
Equipment: equipment,
|
|
Stats: heroState.Stats,
|
|
Skills: heroState.Skills,
|
|
//TODO: active left & right skill should be loaded from save file instead
|
|
LeftSkill: heroState.Skills[attackSkillID],
|
|
RightSkill: heroState.Skills[attackSkillID],
|
|
name: name,
|
|
Class: heroType,
|
|
//nameLabel: d2ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic),
|
|
isRunToggled: false,
|
|
isInTown: true,
|
|
isRunning: false,
|
|
}
|
|
|
|
result.mapEntity.uuid = id
|
|
result.SetSpeed(baseRunSpeed)
|
|
result.mapEntity.directioner = result.rotate
|
|
err = composite.SetMode(d2enum.PlayerAnimationModeTownNeutral, equipment.RightHand.GetWeaponClass())
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
composite.SetDirection(direction)
|
|
|
|
if err := composite.Equip(layerEquipment); err != nil {
|
|
fmt.Printf("failed to equip, err: %v\n", err)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// NewMissile creates a new Missile and initializes it's animation.
|
|
func (f *MapEntityFactory) NewMissile(x, y int, record *d2records.MissileRecord) (*Missile, error) {
|
|
animation, err := f.asset.LoadAnimation(
|
|
fmt.Sprintf("%s/%s.dcc", d2resource.MissileData, record.Animation.CelFileName),
|
|
d2resource.PaletteUnits,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if record.Animation.HasSubLoop {
|
|
animation.SetSubLoop(record.Animation.SubStartingFrame, record.Animation.SubEndingFrame)
|
|
}
|
|
|
|
animation.SetEffect(d2enum.DrawEffectModulate)
|
|
animation.SetPlayLoop(record.Animation.LoopAnimation)
|
|
animation.PlayForward()
|
|
entity := NewAnimatedEntity(x, y, animation)
|
|
|
|
result := &Missile{
|
|
AnimatedEntity: entity,
|
|
record: record,
|
|
}
|
|
result.Speed = float64(record.Velocity)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// NewItem creates an item map entity
|
|
func (f *MapEntityFactory) NewItem(x, y int, codes ...string) (*Item, error) {
|
|
item, err := f.item.NewItem(codes...)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filename := item.CommonRecord().FlippyFile
|
|
filepath := fmt.Sprintf("%s/%s.DC6", d2resource.ItemGraphics, filename)
|
|
animation, err := f.asset.LoadAnimation(filepath, d2resource.PaletteUnits)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
animation.PlayForward()
|
|
animation.SetPlayLoop(false)
|
|
entity := NewAnimatedEntity(x*5, y*5, animation)
|
|
|
|
result := &Item{
|
|
AnimatedEntity: entity,
|
|
Item: item,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// NewNPC creates a new NPC and returns a pointer to it.
|
|
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatsRecord, direction int) (*NPC, error) {
|
|
result := &NPC{
|
|
mapEntity: newMapEntity(x, y),
|
|
HasPaths: false,
|
|
monstatRecord: monstat,
|
|
monstatEx: f.asset.Records.Monster.Stats2[monstat.ExtraDataKey],
|
|
}
|
|
|
|
var equipment [16]string
|
|
|
|
for compType, opts := range result.monstatEx.EquipmentOptions {
|
|
equipment[compType] = selectEquip(opts)
|
|
}
|
|
|
|
composite, err := f.asset.LoadComposite(d2enum.ObjectTypeCharacter, monstat.AnimationDirectoryToken,
|
|
d2resource.PaletteUnits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.composite = composite
|
|
|
|
if err := composite.SetMode(d2enum.MonsterAnimationModeNeutral,
|
|
result.monstatEx.BaseWeaponClass); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := composite.Equip(&equipment); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result.SetSpeed(float64(monstat.SpeedBase))
|
|
result.mapEntity.directioner = result.rotate
|
|
|
|
result.composite.SetDirection(direction)
|
|
|
|
if result.monstatRecord != nil && result.monstatRecord.IsInteractable {
|
|
result.name = d2tbl.TranslateString(result.monstatRecord.NameString)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// NewCastOverlay creates a cast overlay map entity
|
|
func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.OverlayRecord) (*CastOverlay, error) {
|
|
animation, err := f.asset.LoadAnimationWithEffect(
|
|
fmt.Sprintf("/data/Global/Overlays/%s.dcc", overlayRecord.Filename),
|
|
d2resource.PaletteUnits,
|
|
d2enum.DrawEffectModulate,
|
|
)
|
|
|
|
// TODO: Frame index and played count seem to be shared across the cloned animation objects when we retrieve the animation from the asset manager cache.
|
|
animation.Rewind()
|
|
animation.ResetPlayedCount()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
animationSpeed := float64(overlayRecord.AnimRate*25.0) / 1000.0
|
|
playLoop := false // TODO: should be based on the overlay record, some overlays can repeat(e.g. Bone Shield, Frozen Armor)
|
|
|
|
animation.SetPlayLength(animationSpeed)
|
|
animation.SetPlayLoop(playLoop)
|
|
animation.PlayForward()
|
|
|
|
targetX := x + overlayRecord.XOffset
|
|
targetY := y + overlayRecord.YOffset
|
|
|
|
entity := NewAnimatedEntity(targetX, targetY, animation)
|
|
|
|
result := &CastOverlay{
|
|
AnimatedEntity: entity,
|
|
record: overlayRecord,
|
|
playLoop: playLoop,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// NewObject creates an instance of AnimatedComposite
|
|
func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailsRecord,
|
|
palettePath string) (*Object, error) {
|
|
locX, locY := float64(x), float64(y)
|
|
entity := &Object{
|
|
uuid: uuid.New().String(),
|
|
objectRecord: objectRec,
|
|
Position: d2vector.NewPosition(locX, locY),
|
|
name: d2tbl.TranslateString(objectRec.Name),
|
|
}
|
|
objectType := f.asset.Records.Object.Types[objectRec.Index]
|
|
|
|
composite, err := f.asset.LoadComposite(d2enum.ObjectTypeItem, objectType.Token,
|
|
palettePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entity.composite = composite
|
|
|
|
err = entity.setMode(d2enum.ObjectAnimationModeNeutral, 0, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = initObject(entity)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return entity, nil
|
|
}
|