1 /**
2  * Space shooter demo.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.demo.pewpew.pewpew;
15 
16 import std.algorithm.iteration;
17 import std.format : format;
18 import std.math;
19 import std.random;
20 import std.datetime;
21 import std.algorithm : min;
22 import std.conv;
23 import std.range;
24 import std.traits, std.typecons;
25 
26 import ae.ui.app.application;
27 import ae.ui.app.main;
28 import ae.ui.audio.mixer.software;
29 import ae.ui.audio.sdl2.audio;
30 import ae.ui.audio.source.memory;
31 import ae.ui.audio.source.wave;
32 import ae.ui.shell.shell;
33 import ae.ui.shell.sdl2.shell;
34 import ae.ui.video.bmfont;
35 import ae.ui.video.video;
36 import ae.ui.video.sdl2.video;
37 import ae.ui.video.renderer;
38 import ae.utils.graphics.draw;
39 import ae.utils.graphics.fonts.font8x8;
40 import ae.utils.graphics.gamma;
41 import ae.utils.fps;
42 import ae.utils.meta;
43 import ae.utils.sound.wave;
44 
45 import ae.demo.pewpew.objects;
46 
47 final class MyApplication : Application
48 {
49 	override string getName() { return "Demo/PewPew"; }
50 	override string getCompanyName() { return "CyberShadow"; }
51 
52 	Shell shell;
53 	uint ticks;
54 	alias GammaRamp!(COLOR.ChannelType, ubyte) MyGamma;
55 	MyGamma gamma;
56 	FPSCounter fps;
57 
58 	static uint currentTick() { return TickDuration.currSystemTick().to!("msecs", uint)(); }
59 
60 	enum InputSource { keyboard, joystick, max }
61 	enum GameKey { up, down, left, right, fire, none }
62 
63 	int[InputSource.max][GameKey.max] inputMatrix;
64 
65 	MemorySoundSource!short sndShoot, sndWarpIn, sndTorpedoHit, sndEnemyFire;
66 	FontTextureSource!Font8x8 font;
67 	int highScore;
68 
69 	override void render(Renderer s)
70 	{
71 		fps.tick(&shell.setCaption);
72 
73 		{
74 			auto screenCanvas = s.lock();
75 			scope(exit) s.unlock();
76 
77 			if (initializing)
78 			{
79 				gamma = MyGamma(ColorSpace.sRGB);
80 				new Game();
81 				foreach (i; 0..1000) step(10);
82 				ticks = currentTick();
83 				initializing = false;
84 			}
85 
86 			foreach (i, key; EnumMembers!GameKey[0..$-1])
87 			{
88 				enum name = __traits(allMembers, GameKey)[i];
89 				bool pressed;
90 				foreach (input; inputMatrix[key])
91 					if (input)
92 						pressed = true;
93 				mixin(name ~ " = pressed;");
94 			}
95 
96 			//auto destTicks = ticks+deltaTicks;
97 			uint destTicks = currentTick();
98 			// step(deltaTicks);
99 			while (ticks < destTicks)
100 				ticks++,
101 				step(1);
102 
103 			auto canvasSize = min(screenCanvas.w, screenCanvas.h);
104 			canvas.size(canvasSize, canvasSize);
105 			canvas.fill(canvas.COLOR.init);
106 			foreach (ref plane; planes)
107 				foreach (obj; plane)
108 					obj.render();
109 
110 			auto x = (screenCanvas.w-canvasSize)/2;
111 			auto y = (screenCanvas.h-canvasSize)/2;
112 			auto dest = screenCanvas.crop(x, y, x+canvasSize, y+canvasSize);
113 
114 			import std.parallelism;
115 			import std.range;
116 			foreach (j; taskPool.parallel(iota(canvasSize)))
117 			{
118 				auto src = canvas.crop(0, j, canvasSize, j+1);
119 				auto ramp = gamma.lum2pixValues.ptr;
120 				src.colorMap!(c => Renderer.COLOR.monochrome(ramp[c.l])).blitTo(dest, 0, j);
121 			}
122 		}
123 
124 		if (highScore < score)
125 		{
126 			highScore = score;
127 			config.write("HighScore", highScore);
128 			config.save();
129 		}
130 		font.drawText(s, 4,  4, "     Score: %08d".format(    score));
131 		font.drawText(s, 4, 12, "High Score: %08d".format(highScore));
132 		
133 		foreach (sound; sounds)
134 			final switch (sound)
135 			{
136 				case Sound.fire      : shell.audio.mixer.playSound(sndShoot     ); break;
137 				case Sound.warpIn    : shell.audio.mixer.playSound(sndWarpIn    ); break;
138 				case Sound.torpedoHit: shell.audio.mixer.playSound(sndTorpedoHit); break;
139 				case Sound.enemyFire : shell.audio.mixer.playSound(sndEnemyFire ); break;
140 				case Sound.explosion : playExplosion();                            break;
141 			}
142 		sounds = null;
143 	}
144 
145 	void step(uint deltaTicks)
146 	{
147 		foreach (ref plane; planes)
148 			foreach (obj; plane)
149 				obj.step(deltaTicks);
150 	}
151 
152 	GameKey keyToGameKey(Key key)
153 	{
154 		switch (key)
155 		{
156 			case Key.up   : return GameKey.up   ;
157 			case Key.down : return GameKey.down ;
158 			case Key.left : return GameKey.left ;
159 			case Key.right: return GameKey.right;
160 			case Key.space: return GameKey.fire ;
161 			default       : return GameKey.none ;
162 		}
163 	}
164 
165 	override void handleKeyDown(Key key, dchar character)
166 	{
167 		auto gameKey = keyToGameKey(key);
168 		if (gameKey != GameKey.none)
169 			inputMatrix[gameKey][InputSource.keyboard]++;
170 		else
171 		if (key == Key.esc)
172 			shell.quit();
173 	}
174 
175 	override void handleKeyUp(Key key)
176 	{
177 		auto gameKey = keyToGameKey(key);
178 		if (gameKey != GameKey.none)
179 			inputMatrix[gameKey][InputSource.keyboard] = 0;
180 	}
181 
182 	override bool needJoystick() { return true; }
183 
184 	int[2] axisInitial;
185 	bool[2] axisCalibrated;
186 
187 	override void handleJoyAxisMotion(int axis, short svalue)
188 	{
189 		if (axis >= 2) return;
190 
191 		int value = svalue;
192 		if (!axisCalibrated[axis]) // assume first input event is inert
193 			axisInitial[axis] = value,
194 			axisCalibrated[axis] = true;
195 		value -= axisInitial[axis];
196 
197 		import ae.utils.math;
198 		if (abs(value) > short.max/2) // hack?
199 			useAnalog = true;
200 		auto fvalue = bound(cast(float)value / short.max, -1f, 1f);
201 		(axis==0 ? analogX : analogY) = fvalue;
202 	}
203 
204 	JoystickHatState lastState = cast(JoystickHatState)0;
205 
206 	override void handleJoyHatMotion (int hat, JoystickHatState state)
207 	{
208 		void checkDirection(JoystickHatState direction, GameKey key)
209 		{
210 			if (!(lastState & direction) && (state & direction)) inputMatrix[key][InputSource.joystick]++;
211 			if ((lastState & direction) && !(state & direction)) inputMatrix[key][InputSource.joystick]--;
212 		}
213 		checkDirection(JoystickHatState.up   , GameKey.up   );
214 		checkDirection(JoystickHatState.down , GameKey.down );
215 		checkDirection(JoystickHatState.left , GameKey.left );
216 		checkDirection(JoystickHatState.right, GameKey.right);
217 		lastState = state;
218 	}
219 
220 	override void handleJoyButtonDown(int button)
221 	{
222 		inputMatrix[GameKey.fire][InputSource.joystick]++;
223 	}
224 
225 	override void handleJoyButtonUp  (int button)
226 	{
227 		inputMatrix[GameKey.fire][InputSource.joystick]--;
228 	}
229 
230 	private final void genSounds()
231 	{
232 		enum sr = 44100;
233 		sndShoot = (sr*2/3)
234 			.I!(dur =>
235 				dur.iota.map!(n =>
236 					cast(short)(whiteNoiseSqr!short[cast(size_t)(n / (n / 10000.0 + 5))] / 8)
237 			)).retro.fade.array.memorySoundSource(sr);
238 
239 		{
240 			enum freq = 1000;
241 			sndWarpIn = (sr*3).I!(dur => dur.iota.map!(n => n % freq < (freq * n / dur) ? short.init : short.max))
242 				.array.memorySoundSource(sr);
243 		}
244 		
245 		sndTorpedoHit = (sr*2/3).I!(dur => dur.iota.map!(n => short(whiteNoiseSqr!short[cast(size_t)(n / (n / 10000.0 + 5))] / 4))).fade
246 			.array.memorySoundSource(sr);
247 
248 		sndEnemyFire = (sr/3).iota.map!(n => short(squareWave!short(n / 1500.0 + 30)[n] / 4)).fade
249 			.array.memorySoundSource(sr);
250 	}
251 
252 	private final void playExplosion()
253 	{
254 		enum sr = 44100;
255 		auto freq = uniform(50, 100);
256 		auto w = (sr*2).I!(dur => dur.iota.map!(n => short(whiteNoiseSqr!short[cast(size_t)(n / (n / 10000.0 + freq))] / 8))).fade;
257 		shell.audio.mixer.playSound(w.waveSoundSource(sr));
258 	}
259 
260 	override int run(string[] args)
261 	{
262 		genSounds();
263 		font = new FontTextureSource!Font8x8(font8x8, BGRX(128, 128, 128));
264 		highScore = config.read("HighScore", 0);
265 
266 		shell = new SDL2Shell(this);
267 		shell.video = new SDL2SoftwareVideo();
268 
269 		shell.audio = new SDL2Audio();
270 		shell.audio.mixer = new SoftwareMixer();
271 
272 		shell.run();
273 		shell.video.shutdown();
274 		return 0;
275 	}
276 
277 	override void handleQuit()
278 	{
279 		shell.quit();
280 	}
281 }
282 
283 shared static this()
284 {
285 	createApplication!MyApplication();
286 }