1 ///
2 module isodi.raylib.camera;
3 
4 import std.conv;
5 import std.meta;
6 import std.traits;
7 
8 import raylib;
9 
10 import isodi.camera : Camera;
11 import isodi.object3d;
12 
13 
14 @safe:
15 
16 
17 private {
18 
19     struct Affects(string what) {
20         static immutable name = what;
21     }
22     struct Change(short by) {
23         static immutable value = by;
24     }
25     enum Speed;
26 
27 }
28 
29 
30 /// Keys to connect to camera actions.
31 struct CameraKeybindings {
32 
33     @Affects!"distance" {
34 
35         /// Zoom the camera in.
36         @Change!(-1)
37         KeyboardKey zoomIn;
38 
39         /// Zoom the camera out
40         @Change!(+1)
41         KeyboardKey zoomOut;
42 
43     }
44 
45     @Affects!"angle.x" {
46 
47         /// Rotate the camera clockwise.
48         @Change!(-1)
49         KeyboardKey rotateRight;
50 
51         /// Rotate the camera counter-clockwise.
52         @Change!(+1)
53         KeyboardKey rotateLeft;
54 
55     }
56 
57     @Affects!"angle.y" {
58 
59         /// Rotate the camera down on the Y axis.
60         @Change!(-1)
61         KeyboardKey rotateDown;
62 
63         /// Rotate the camera up on the Y axis.
64         @Change!(+1)
65         KeyboardKey rotateUp;
66 
67     }
68 
69     @Affects!"offsetScreenX" {
70 
71         /// Move the camera towards negative X.
72         @Change!(-1)
73         KeyboardKey moveLeft;
74 
75         /// Move the camera towards positive X.
76         @Change!(+1)
77         KeyboardKey moveRight;
78 
79     }
80 
81     @Affects!"offsetScreenY" {
82 
83         /// Move the camera towards positive Y.
84         @Change!(+1)
85         KeyboardKey moveDown;
86 
87         /// Move the camera towards negative Y.
88         @Change!(-1)
89         KeyboardKey moveUp;
90 
91     }
92 
93     @Affects!"offset.height" {
94 
95         /// Move the camera downwards.
96         @Change!(-1)
97         KeyboardKey moveBelow;
98 
99         /// Move the camera upwards.
100         @Change!(+1)
101         KeyboardKey moveAbove;
102 
103     }
104 
105     @Speed {
106 
107         /// Zoom speed, cell per second.
108         @Affects!"distance"
109         float zoomSpeed = 10;
110 
111         /// Rotation speed, degrees per second.
112         @Affects!"angle.x"
113         @Affects!"angle.y"
114         float rotateSpeed = 90;
115 
116         /// Movement speed, cells per second.
117         @Affects!"offsetScreenX"
118         @Affects!"offsetScreenY"
119         @Affects!"offset.height"
120         float movementSpeed = 4;
121 
122     }
123 
124     /// Maximum zoom in.
125     float zoomInLimit = 5;
126 
127     /// Maximum zoom out. This should be greater than `zoomInLimit`.
128     float zoomOutLimit = 50;
129 
130 }
131 
132 /// Helper for quickly binding keys to freely move the camera.
133 void updateCamera(ref Camera camera, CameraKeybindings keybinds) @trusted {
134 
135     assert(camera.follow !is null,
136         "camera.follow must be set for updateCamera to work, and it must use the Object3D.Implement mixin");
137 
138     alias SpeedFields = getSymbolsByUDA!(CameraKeybindings, Speed);
139 
140     const delta = GetFrameTime();
141 
142     // Check each field
143     static foreach (actionName; FieldNameTuple!CameraKeybindings) {{
144 
145         alias BindField = mixin("CameraKeybindings." ~ actionName);
146 
147         // This is a key field
148         static if (hasUDA!(BindField, Change)) {
149 
150             alias ChangeDeco = getUDAs!(BindField, Change)[0];
151             enum direction = ChangeDeco.value;
152 
153             // Pressed
154             if (mixin("keybinds." ~ actionName).IsKeyDown) {
155 
156                 // Get the affected field
157                 alias AffectDeco = getUDAs!(BindField, Affects)[0];
158                 enum cameraField = "camera." ~ AffectDeco.name;
159                 alias CameraField = typeof(mixin(cameraField));
160 
161                 // Get the speed field for the deco
162                 alias HasAffectDeco = ApplyRight!(hasUDA, AffectDeco);
163                 alias speedSymbol = Filter!(HasAffectDeco, SpeedFields)[0];
164                 enum speedField = __traits(identifier, speedSymbol);
165 
166                 // Get the total change
167                 const change = direction * delta * mixin("keybinds." ~ speedField);
168 
169                 // Check if the given type is a field or property
170                 enum IsField(alias T) = !isFunction!T || functionAttributes!T & FunctionAttribute.property;
171 
172                 // If so
173                 static if (IsField!(mixin(cameraField))) {
174 
175                     import std.algorithm : clamp;
176 
177                     // Get the new value
178                     auto newValue = mixin(cameraField) + change;
179 
180                     // Clamp angle Y
181                     static if (cameraField == "camera.angle.y") {
182 
183                         newValue = newValue.clamp(0, 90);
184 
185                     }
186 
187                     // Clamp camera distance
188                     else static if (cameraField == "camera.distance") {
189 
190                         newValue = newValue.clamp(keybinds.zoomInLimit, keybinds.zoomOutLimit);
191 
192                     }
193 
194                     // Increment the field
195                     mixin(cameraField) = newValue;
196 
197                 }
198 
199                 // Call the function otherwise
200                 else mixin(cameraField)(change);
201 
202             }
203 
204         }
205 
206     }}
207 
208 }
209 
210 /// Apply a billboard effect on the current object.
211 void applyBillboard(ref Camera camera) @trusted {
212 
213     // Rotate to counter the camera
214     rlRotatef(camera.angle.x, 0, 1, 0);
215     rlRotatef(-camera.angle.y, 1, 0, 0);
216 
217 }