1+ package net .cytonic .cytosis .entity .npc ;
2+
3+ import java .util .ArrayList ;
4+ import java .util .List ;
5+ import java .util .Map ;
6+ import java .util .UUID ;
7+ import java .util .concurrent .ConcurrentHashMap ;
8+
9+ import lombok .Getter ;
10+ import net .kyori .adventure .text .Component ;
11+ import net .kyori .adventure .text .format .NamedTextColor ;
12+ import net .minestom .server .MinecraftServer ;
13+ import net .minestom .server .coordinate .Pos ;
14+ import net .minestom .server .coordinate .Vec ;
15+ import net .minestom .server .entity .EntityType ;
16+ import net .minestom .server .entity .GameMode ;
17+ import net .minestom .server .entity .MetadataDef ;
18+ import net .minestom .server .entity .ai .EntityAIGroup ;
19+ import net .minestom .server .network .packet .server .play .DestroyEntitiesPacket ;
20+ import net .minestom .server .network .packet .server .play .EntityHeadLookPacket ;
21+ import net .minestom .server .network .packet .server .play .EntityRotationPacket ;
22+ import net .minestom .server .network .packet .server .play .EntityTeleportPacket ;
23+ import net .minestom .server .network .packet .server .play .PlayerInfoRemovePacket ;
24+ import net .minestom .server .network .packet .server .play .PlayerInfoUpdatePacket ;
25+ import net .minestom .server .network .packet .server .play .SpawnEntityPacket ;
26+ import net .minestom .server .network .packet .server .play .TeamsPacket ;
27+ import net .minestom .server .timer .TaskSchedule ;
28+ import org .jetbrains .annotations .ApiStatus .OverrideOnly ;
29+
30+ import net .cytonic .cytosis .Cytosis ;
31+ import net .cytonic .cytosis .entity .hologram .PlayerHolograms ;
32+ import net .cytonic .cytosis .entity .npc .configuration .NPCConfiguration ;
33+ import net .cytonic .cytosis .entity .npc .dialogs .Dialog ;
34+ import net .cytonic .cytosis .entity .npc .impl .NPCEntityImpl ;
35+ import net .cytonic .cytosis .entity .npc .pathfinding .NavigatePathGoal ;
36+ import net .cytonic .cytosis .entity .npc .pathfinding .Path ;
37+ import net .cytonic .cytosis .events .npcs .NPCInteractEvent ;
38+ import net .cytonic .cytosis .managers .NpcManager ;
39+ import net .cytonic .cytosis .player .CytosisPlayer ;
40+ import net .cytonic .cytosis .utils .MetadataPacketBuilder ;
41+
42+ public abstract class NPC {
43+
44+ public static final int SPAWN_DISTANCE = 48 ;
45+ public static final int SPAWN_DISTANCE_SQUARED = SPAWN_DISTANCE * SPAWN_DISTANCE ;
46+ public static final int LOOK_DISTANCE = 5 ;
47+ public static final int LOOK_DISTANCE_SQUARED = LOOK_DISTANCE * LOOK_DISTANCE ;
48+
49+ @ Getter
50+ private final UUID uuid = UUID .randomUUID ();
51+ @ Getter
52+ private final NPCConfiguration config ;
53+ @ Getter
54+ private final Component name ;
55+
56+ private final Map <UUID , ViewerData > viewers = new ConcurrentHashMap <>();
57+ private final Map <UUID , Dialog <? extends CytosisPlayer >> activeDialogs = new ConcurrentHashMap <>();
58+
59+ public NPC (NPCConfiguration config ) {
60+ this .config = config ;
61+ String className = getClass ().getSimpleName ().replaceAll ("NPC" , "" );
62+ this .name =
63+ config .chatName () != null ? config .chatName ()
64+ : Component .text (className .replaceAll ("(?<=.)(?=\\ p{Lu})" , " " ));
65+ }
66+
67+ @ OverrideOnly
68+ public void onClick (NPCInteractEvent event ) {
69+ }
70+
71+ public boolean hasActiveDialog (CytosisPlayer player ) {
72+ return activeDialogs .containsKey (player .getUuid ());
73+ }
74+
75+ protected <P extends CytosisPlayer > Dialog <P > startDialog (P player ) {
76+ if (activeDialogs .containsKey (player .getUuid ())) {
77+ //noinspection unchecked
78+ return (Dialog <P >) activeDialogs .get (player .getUuid ());
79+ }
80+ Dialog <P > dialog = new Dialog <>(this );
81+ activeDialogs .put (player .getUuid (), dialog );
82+ return dialog ;
83+ }
84+
85+ public void register () {
86+ Cytosis .get (NpcManager .class ).register (this );
87+ }
88+
89+ public void remove () {
90+ viewers .forEach ((uuid , data ) ->
91+ Cytosis .getPlayer (uuid ).ifPresent (player -> {
92+ player .sendPacket (new DestroyEntitiesPacket (List .of (data .entity ().getEntityId ())));
93+ PlayerHolograms .removeHologram (data .hologram ());
94+ }));
95+ viewers .clear ();
96+ Cytosis .get (NpcManager .class ).removeNPC (this );
97+ }
98+
99+ public void updateForPlayer (CytosisPlayer player ) {
100+ Pos npcPos = config .position (player );
101+ double distanceSquared = player .getPosition ().distanceSquared (npcPos );
102+ boolean inRange = distanceSquared <= SPAWN_DISTANCE_SQUARED ;
103+
104+ ViewerData data = viewers .get (player .getUuid ());
105+
106+ if (data == null ) {
107+ if (!inRange ) return ;
108+
109+ // has not been loaded by player before
110+ List <Component > holograms = config .holograms (player );
111+ String username = "" ;
112+
113+ NPCEntityImpl entity = new NPCEntityImpl (
114+ username ,
115+ config .texture (player ),
116+ config .signature (player ),
117+ holograms
118+ );
119+ entity .setInstance (config .instance (), npcPos );
120+ Path path = config .path (player );
121+ if (path != null ) {
122+ EntityAIGroup group = new EntityAIGroup ();
123+ group .getGoalSelectors ().add (new NavigatePathGoal (entity , path ));
124+ entity .addAIGroup (group );
125+ }
126+
127+ sendPackets (player , entity );
128+
129+ MinecraftServer .getSchedulerManager ()
130+ .scheduleTask (() -> player .sendPacket (new PlayerInfoRemovePacket (entity .getUuid ())),
131+ TaskSchedule .tick (2 ), TaskSchedule .stop ());
132+
133+ PlayerHolograms .Hologram hologram = PlayerHolograms .Hologram .builder ()
134+ .pos (config .position (player ).add (0 , 2.0 , 0 ))
135+ .lines (holograms )
136+ .player (player )
137+ .instance (config .instance ())
138+ .build ();
139+
140+ PlayerHolograms .addHologram (hologram );
141+ viewers .put (player .getUuid (), new ViewerData (entity , hologram ));
142+ return ;
143+ }
144+
145+ // player has seen it before
146+ NPCEntityImpl entity = data .entity ();
147+ PlayerHolograms .Hologram hologram = data .hologram ();
148+
149+ if (!inRange ) {
150+ player .sendPacket (new DestroyEntitiesPacket (List .of (entity .getEntityId ())));
151+ PlayerHolograms .removeHologram (hologram );
152+ viewers .remove (player .getUuid ());
153+ return ;
154+ }
155+
156+ player .sendPacket (new EntityTeleportPacket (entity .getEntityId (), entity .getPosition (), Vec .ZERO , 0 , false ));
157+ PlayerHolograms .updateHologram (hologram , entity .getPosition ().add (0 , 2.0 , 0 ));
158+
159+ if (config .looking (player ) && distanceSquared <= LOOK_DISTANCE_SQUARED ) {
160+ Pos lookPos = entity .getPosition ().withLookAt (player .getPosition ().add (0 , player .getEyeHeight (), 0 ));
161+ player .sendPackets (
162+ new EntityRotationPacket (entity .getEntityId (), lookPos .yaw (), lookPos .pitch (), player .isOnGround ()),
163+ new EntityHeadLookPacket (entity .getEntityId (), lookPos .yaw ()));
164+ } else {
165+ player .sendPackets (
166+ new EntityRotationPacket (entity .getEntityId (), entity .getPosition ().yaw (),
167+ entity .getPosition ().pitch (), player .isOnGround ()),
168+ new EntityHeadLookPacket (entity .getEntityId (), entity .getPosition ().yaw ()));
169+ }
170+ }
171+
172+ public void removePlayer (CytosisPlayer player ) {
173+ activeDialogs .remove (player .getUuid ());
174+ ViewerData data = viewers .remove (player .getUuid ());
175+ if (data != null ) {
176+ player .sendPacket (new DestroyEntitiesPacket (List .of (data .entity ().getEntityId ())));
177+ PlayerHolograms .removeHologram (data .hologram ());
178+ }
179+ }
180+
181+ public void removeDialog (CytosisPlayer player ) {
182+ activeDialogs .remove (player .getUuid ());
183+ }
184+
185+ public boolean hasEntityId (CytosisPlayer player , int entityId ) {
186+ ViewerData data = viewers .get (player .getUuid ());
187+ return data != null && data .entity ().getEntityId () == entityId ;
188+ }
189+
190+ private void sendPackets (CytosisPlayer player , NPCEntityImpl entity ) {
191+ List <PlayerInfoUpdatePacket .Property > properties = new ArrayList <>();
192+ if (entity .getSkinTexture () != null && entity .getSkinSignature () != null ) {
193+ properties .add (new PlayerInfoUpdatePacket .Property ("textures" , entity .getSkinTexture (),
194+ entity .getSkinSignature ()));
195+ }
196+ Pos npcPos = entity .getPosition ();
197+ player .sendPackets (
198+ new PlayerInfoUpdatePacket (PlayerInfoUpdatePacket .Action .ADD_PLAYER ,
199+ new PlayerInfoUpdatePacket .Entry (
200+ entity .getUuid (),
201+ entity .getUsername (),
202+ properties ,
203+ false ,
204+ 0 ,
205+ GameMode .ADVENTURE ,
206+ Component .empty (),
207+ null ,
208+ 0 ,
209+ false )),
210+ new SpawnEntityPacket (entity .getEntityId (), entity .getUuid (), EntityType .PLAYER ,
211+ npcPos ,
212+ npcPos .yaw (),
213+ 0 ,
214+ Vec .ZERO ),
215+ new EntityHeadLookPacket (entity .getEntityId (), npcPos .yaw ()),
216+ MetadataPacketBuilder .empty (entity .getEntityId ())
217+ .setByte (MetadataDef .Avatar .DISPLAYED_MODEL_PARTS_FLAGS .index (), (byte ) 127 )
218+ .setBoolean (MetadataDef .Player .HAS_NO_GRAVITY .index (), true )
219+ .build (),
220+ new TeamsPacket ("npc_team" , new TeamsPacket .CreateTeamAction (
221+ Component .text ("NPCs" ),
222+ (byte ) 0 ,
223+ TeamsPacket .NameTagVisibility .NEVER ,
224+ TeamsPacket .CollisionRule .NEVER ,
225+ NamedTextColor .WHITE ,
226+ Component .empty (),
227+ Component .empty (),
228+ List .of (entity .getUsername ())
229+ ))
230+ );
231+ }
232+
233+ private record ViewerData (NPCEntityImpl entity , PlayerHolograms .Hologram hologram ) {
234+
235+ }
236+ }
0 commit comments