-
Notifications
You must be signed in to change notification settings - Fork 16
/
MouseCursorRenderer.cs
379 lines (331 loc) · 15.3 KB
/
MouseCursorRenderer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
using EndlessClient.Controllers;
using EndlessClient.Dialogs;
using EndlessClient.HUD;
using EndlessClient.Input;
using EndlessClient.Rendering.Character;
using EOLib;
using EOLib.Domain.Character;
using EOLib.Domain.Item;
using EOLib.Domain.Map;
using EOLib.Graphics;
using EOLib.IO;
using EOLib.IO.Map;
using EOLib.IO.Pub;
using EOLib.IO.Repositories;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Optional;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using XNAControls;
namespace EndlessClient.Rendering
{
public class MouseCursorRenderer : XNAControl, IMouseCursorRenderer
{
private enum CursorIndex
{
Standard = 0,
HoverNormal = 1,
HoverItem = 2,
ClickFirstFrame = 3,
ClickSecondFrame = 4,
NumberOfFramesInSheet = 5
}
private readonly Rectangle SingleCursorFrameArea;
private readonly Texture2D _mouseCursorTexture;
private readonly ICharacterProvider _characterProvider;
private readonly IRenderOffsetCalculator _renderOffsetCalculator;
private readonly IMapCellStateProvider _mapCellStateProvider;
private readonly IItemStringService _itemStringService;
private readonly IItemNameColorService _itemNameColorService;
private readonly IEIFFileProvider _eifFileProvider;
private readonly ICurrentMapProvider _currentMapProvider;
private readonly IMapInteractionController _mapInteractionController;
private readonly IUserInputProvider _userInputProvider;
private readonly IActiveDialogProvider _activeDialogProvider;
private readonly IContextMenuProvider _contextMenuProvider;
private readonly XNALabel _mapItemText;
private int _gridX, _gridY;
private CursorIndex _cursorIndex;
private bool _shouldDrawCursor;
private Option<DateTime> _startClickTime;
private CursorIndex _clickFrame;
private int _clickAlpha;
private Option<MapCoordinate> _clickCoordinate;
public MapCoordinate GridCoordinates => new MapCoordinate(_gridX, _gridY);
public MouseCursorRenderer(INativeGraphicsManager nativeGraphicsManager,
ICharacterProvider characterProvider,
ICharacterRendererProvider characterRendererProvider,
IRenderOffsetCalculator renderOffsetCalculator,
IMapCellStateProvider mapCellStateProvider,
IItemStringService itemStringService,
IItemNameColorService itemNameColorService,
IEIFFileProvider eifFileProvider,
ICurrentMapProvider currentMapProvider,
IMapInteractionController mapInteractionController,
IUserInputProvider userInputProvider,
IActiveDialogProvider activeDialogProvider,
IContextMenuProvider contextMenuProvider)
{
_mouseCursorTexture = nativeGraphicsManager.TextureFromResource(GFXTypes.PostLoginUI, 24, true);
_characterProvider = characterProvider;
_renderOffsetCalculator = renderOffsetCalculator;
_mapCellStateProvider = mapCellStateProvider;
_itemStringService = itemStringService;
_itemNameColorService = itemNameColorService;
_eifFileProvider = eifFileProvider;
_currentMapProvider = currentMapProvider;
_mapInteractionController = mapInteractionController;
_userInputProvider = userInputProvider;
_activeDialogProvider = activeDialogProvider;
_contextMenuProvider = contextMenuProvider;
SingleCursorFrameArea = new Rectangle(0, 0,
_mouseCursorTexture.Width/(int) CursorIndex.NumberOfFramesInSheet,
_mouseCursorTexture.Height);
DrawArea = SingleCursorFrameArea;
_mapItemText = new XNALabel(Constants.FontSize09)
{
Visible = false,
Text = string.Empty,
ForeColor = Color.White,
AutoSize = false,
DrawOrder = 10 //todo: make a better provider for draw orders (see also HudControlsFactory)
};
_clickCoordinate = Option.None<MapCoordinate>();
}
public override void Initialize()
{
_mapItemText.AddControlToDefaultGame();
}
#region Update and Helpers
public override void Update(GameTime gameTime)
{
// prevents updates if there is a dialog
if (!ShouldUpdate() || _activeDialogProvider.ActiveDialogs.Any(x => x.HasValue) ||
_contextMenuProvider.ContextMenu.HasValue)
return;
// todo: don't do anything if there is a context menu and mouse is over context menu
var offsetX = MainCharacterOffsetX();
var offsetY = MainCharacterOffsetY();
SetGridCoordsBasedOnMousePosition(offsetX, offsetY);
UpdateDrawPostionBasedOnGridPosition(offsetX, offsetY);
var cellState = _mapCellStateProvider.GetCellStateAt(_gridX, _gridY);
UpdateCursorSourceRectangle(cellState);
CheckForClicks(cellState);
}
private void SetGridCoordsBasedOnMousePosition(int offsetX, int offsetY)
{
//need to solve this system of equations to get x, y on the grid
//(x * 32) - (y * 32) + 288 - c.OffsetX, => pixX = 32x - 32y + 288 - c.OffsetX
//(y * 16) + (x * 16) + 144 - c.OffsetY => 2pixY = 32y + 32x + 288 - 2c.OffsetY
// => 2pixY + pixX = 64x + 576 - c.OffsetX - 2c.OffsetY
// => 2pixY + pixX - 576 + c.OffsetX + 2c.OffsetY = 64x
// => _gridX = (pixX + 2pixY - 576 + c.OffsetX + 2c.OffsetY) / 64; <=
//pixY = (_gridX * 16) + (_gridY * 16) + 144 - c.OffsetY =>
//(pixY - (_gridX * 16) - 144 + c.OffsetY) / 16 = _gridY
var mouseState = _userInputProvider.CurrentMouseState;
var msX = mouseState.X - SingleCursorFrameArea.Width / 2;
var msY = mouseState.Y - SingleCursorFrameArea.Height / 2;
_gridX = (int)Math.Round((msX + 2 * msY - 576 + offsetX + 2 * offsetY) / 64.0);
_gridY = (int)Math.Round((msY - _gridX * 16 - 144 + offsetY) / 16.0);
}
private void UpdateDrawPostionBasedOnGridPosition(int offsetX, int offsetY)
{
var drawPosition = GetDrawCoordinatesFromGridUnits(_gridX, _gridY, offsetX, offsetY);
DrawArea = new Rectangle((int)drawPosition.X,
(int)drawPosition.Y,
DrawArea.Width,
DrawArea.Height);
}
private void UpdateCursorSourceRectangle(IMapCellState cellState)
{
_shouldDrawCursor = true;
_cursorIndex = CursorIndex.Standard;
if (cellState.Character.HasValue || cellState.NPC.HasValue)
_cursorIndex = CursorIndex.HoverNormal;
else if (cellState.Sign.HasValue)
_shouldDrawCursor = false;
else if (cellState.Items.Any())
{
_cursorIndex = CursorIndex.HoverItem;
UpdateMapItemLabel(Option.Some(cellState.Items.First()));
}
else if (cellState.TileSpec != TileSpec.None)
UpdateCursorIndexForTileSpec(cellState.TileSpec);
if (!cellState.Items.Any())
UpdateMapItemLabel(Option.None<MapItem>());
if (_mapItemText.Visible)
{
//relative to cursor DrawPosition, since this control is a parent of MapItemText
_mapItemText.DrawPosition = new Vector2(DrawArea.X + 32 - _mapItemText.ActualWidth / 2f,
DrawArea.Y + -_mapItemText.ActualHeight - 4);
}
_startClickTime.MatchSome(st =>
{
if ((DateTime.Now - st).TotalMilliseconds > 350)
{
_startClickTime = Option.Some(DateTime.Now);
_clickFrame = _clickFrame + 1;
if (_clickFrame != CursorIndex.ClickFirstFrame && _clickFrame != CursorIndex.ClickSecondFrame)
{
_clickFrame = CursorIndex.Standard;
_startClickTime = Option.None<DateTime>();
}
}
});
}
private int MainCharacterOffsetX()
{
return _renderOffsetCalculator.CalculateOffsetX(_characterProvider.MainCharacter.RenderProperties);
}
private int MainCharacterOffsetY()
{
return _renderOffsetCalculator.CalculateOffsetY(_characterProvider.MainCharacter.RenderProperties);
}
//todo: this same logic is in a base map entity renderer. Maybe extract a service out.
private static Vector2 GetDrawCoordinatesFromGridUnits(int x, int y, int cOffX, int cOffY)
{
return new Vector2(x*32 - y*32 + 288 - cOffX, y*16 + x*16 + 144 - cOffY);
}
private void UpdateMapItemLabel(Option<MapItem> item)
{
item.Match(
some: i =>
{
var data = _eifFileProvider.EIFFile[i.ItemID];
var text = _itemStringService.GetStringForMapDisplay(data, i.Amount);
if (!_mapItemText.Visible || _mapItemText.Text != text)
{
_mapItemText.Visible = true;
_mapItemText.Text = text;
_mapItemText.ResizeBasedOnText();
_mapItemText.ForeColor = _itemNameColorService.GetColorForMapDisplay(data);
}
},
none: () =>
{
_mapItemText.Visible = false;
_mapItemText.Text = string.Empty;
});
}
private void UpdateCursorIndexForTileSpec(TileSpec tileSpec)
{
switch (tileSpec)
{
case TileSpec.Wall:
case TileSpec.JammedDoor:
case TileSpec.MapEdge:
case TileSpec.FakeWall:
case TileSpec.VultTypo:
_shouldDrawCursor = false;
break;
case TileSpec.Chest:
case TileSpec.BankVault:
case TileSpec.ChairDown:
case TileSpec.ChairLeft:
case TileSpec.ChairRight:
case TileSpec.ChairUp:
case TileSpec.ChairDownRight:
case TileSpec.ChairUpLeft:
case TileSpec.ChairAll:
case TileSpec.Board1:
case TileSpec.Board2:
case TileSpec.Board3:
case TileSpec.Board4:
case TileSpec.Board5:
case TileSpec.Board6:
case TileSpec.Board7:
case TileSpec.Board8:
case TileSpec.Jukebox:
_cursorIndex = CursorIndex.HoverNormal;
break;
case TileSpec.NPCBoundary:
case TileSpec.Jump:
case TileSpec.Water:
case TileSpec.Arena:
case TileSpec.AmbientSource:
case TileSpec.SpikesStatic:
case TileSpec.SpikesTrap:
case TileSpec.SpikesTimed:
case TileSpec.None:
_cursorIndex = CursorIndex.Standard;
break;
default:
_cursorIndex = CursorIndex.HoverNormal;
break;
}
}
private void CheckForClicks(IMapCellState cellState)
{
if (_userInputProvider.ClickHandled)
return;
var currentMouseState = _userInputProvider.CurrentMouseState;
var previousMouseState = _userInputProvider.PreviousMouseState;
if (currentMouseState.LeftButton == ButtonState.Released && previousMouseState.LeftButton == ButtonState.Pressed)
{
_mapInteractionController.LeftClick(cellState, Option.Some<IMouseCursorRenderer>(this));
}
}
#endregion
public void Draw(SpriteBatch spriteBatch, Vector2 additionalOffset)
{
if (_contextMenuProvider.ContextMenu.HasValue)
return;
if (_shouldDrawCursor && _gridX >= 0 && _gridY >= 0 &&
_gridX <= _currentMapProvider.CurrentMap.Properties.Width &&
_gridY <= _currentMapProvider.CurrentMap.Properties.Height)
{
spriteBatch.Draw(_mouseCursorTexture,
DrawArea.Location.ToVector2() + additionalOffset,
new Rectangle(SingleCursorFrameArea.Width*(int) _cursorIndex,
0,
SingleCursorFrameArea.Width,
SingleCursorFrameArea.Height),
Color.White);
if (_startClickTime.HasValue)
{
_clickCoordinate.MatchSome(c =>
{
var position = GetDrawCoordinatesFromGridUnits(c.X, c.Y, MainCharacterOffsetX(), MainCharacterOffsetY());
spriteBatch.Draw(_mouseCursorTexture,
position + additionalOffset,
SingleCursorFrameArea.WithPosition(new Vector2(SingleCursorFrameArea.Width * (int)_clickFrame, 0)),
Color.FromNonPremultiplied(255, 255, 255, _clickAlpha -= 5));
});
}
}
}
public void AnimateClick()
{
if (_startClickTime.HasValue)
return;
_startClickTime = Option.Some(DateTime.Now);
_clickFrame = CursorIndex.ClickFirstFrame;
_clickAlpha = 200;
_clickCoordinate = Option.Some(new MapCoordinate(_gridX, _gridY));
}
public void ClearTransientRenderables()
{
_mapItemText.Visible = false;
_startClickTime = Option.None<DateTime>();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_spriteBatch.Dispose();
_mapItemText.Dispose();
}
}
}
public interface IMouseCursorRenderer : IXNAControl, IDisposable
{
MapCoordinate GridCoordinates { get; }
void Draw(SpriteBatch spriteBatch, Vector2 additionalOffset);
void AnimateClick();
void ClearTransientRenderables();
}
}