1 /*
2 * Light.c
3 * Brogue
4 *
5 * Created by Brian Walker on 1/21/09.
6 * Copyright 2012. All rights reserved.
7 *
8 * This file is part of Brogue.
9 *
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License as
12 * published by the Free Software Foundation, either version 3 of the
13 * License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Affero General Public License for more details.
19 *
20 * You should have received a copy of the GNU Affero General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 */
23
24 #include "Rogue.h"
25 #include "IncludeGlobals.h"
26
logLights()27 void logLights() {
28
29 short i, j;
30
31 printf(" ");
32 for (i=0; i<COLS-2; i++) {
33 printf("%i", i % 10);
34 }
35 printf("\n");
36 for( j=0; j<DROWS-2; j++ ) {
37 if (j < 10) {
38 printf(" ");
39 }
40 printf("%i: ", j);
41 for( i=0; i<DCOLS-2; i++ ) {
42 if (tmap[i][j].light[0] == 0) {
43 printf(" ");
44 } else {
45 printf("%i", max(0, tmap[i][j].light[0] / 10 - 1));
46 }
47 }
48 printf("\n");
49 }
50 printf("\n");
51 }
52
53 // Returns true if any part of the light hit cells that are in the player's field of view.
paintLight(lightSource * theLight,short x,short y,boolean isMinersLight,boolean maintainShadows)54 boolean paintLight(lightSource *theLight, short x, short y, boolean isMinersLight, boolean maintainShadows) {
55 short i, j, k;
56 short colorComponents[3], randComponent, lightMultiplier;
57 short fadeToPercent, radiusRounded;
58 fixpt radius;
59 char grid[DCOLS][DROWS];
60 boolean dispelShadows, overlappedFieldOfView;
61
62 brogueAssert(rogue.RNG == RNG_SUBSTANTIVE);
63
64 radius = randClump(theLight->lightRadius) * FP_FACTOR / 100;
65 radiusRounded = fp_round(radius);
66
67 randComponent = rand_range(0, theLight->lightColor->rand);
68 colorComponents[0] = randComponent + theLight->lightColor->red + rand_range(0, theLight->lightColor->redRand);
69 colorComponents[1] = randComponent + theLight->lightColor->green + rand_range(0, theLight->lightColor->greenRand);
70 colorComponents[2] = randComponent + theLight->lightColor->blue + rand_range(0, theLight->lightColor->blueRand);
71
72 // the miner's light does not dispel IS_IN_SHADOW,
73 // so the player can be in shadow despite casting his own light.
74 dispelShadows = !maintainShadows && (colorComponents[0] + colorComponents[1] + colorComponents[2]) > 0;
75
76 fadeToPercent = theLight->radialFadeToPercent;
77
78 // zero out only the relevant rectangle of the grid
79 for (i = max(0, x - radiusRounded); i < DCOLS && i < x + radiusRounded; i++) {
80 for (j = max(0, y - radiusRounded); j < DROWS && j < y + radiusRounded; j++) {
81 grid[i][j] = 0;
82 }
83 }
84
85 getFOVMask(grid, x, y, radius, T_OBSTRUCTS_VISION, (theLight->passThroughCreatures ? 0 : (HAS_MONSTER | HAS_PLAYER)),
86 (!isMinersLight));
87
88 overlappedFieldOfView = false;
89
90 for (i = max(0, x - radiusRounded); i < DCOLS && i < x + radiusRounded; i++) {
91 for (j = max(0, y - radiusRounded); j < DROWS && j < y + radiusRounded; j++) {
92 if (grid[i][j]) {
93 lightMultiplier = 100 - (100 - fadeToPercent) * fp_sqrt(((i-x) * (i-x) + (j-y) * (j-y)) * FP_FACTOR) / radius;
94 for (k=0; k<3; k++) {
95 tmap[i][j].light[k] += colorComponents[k] * lightMultiplier / 100;;
96 }
97 if (dispelShadows) {
98 pmap[i][j].flags &= ~IS_IN_SHADOW;
99 }
100 if (pmap[i][j].flags & (IN_FIELD_OF_VIEW | ANY_KIND_OF_VISIBLE)) {
101 overlappedFieldOfView = true;
102 }
103 }
104 }
105 }
106
107 tmap[x][y].light[0] += colorComponents[0];
108 tmap[x][y].light[1] += colorComponents[1];
109 tmap[x][y].light[2] += colorComponents[2];
110
111 if (dispelShadows) {
112 pmap[x][y].flags &= ~IS_IN_SHADOW;
113 }
114
115 return overlappedFieldOfView;
116 }
117
118
119 // sets miner's light strength and characteristics based on rings of illumination, scrolls of darkness and water submersion
updateMinersLightRadius()120 void updateMinersLightRadius() {
121 fixpt base_fraction, fraction, lightRadius;
122
123 lightRadius = 100 * rogue.minersLightRadius;
124
125 if (rogue.lightMultiplier < 0) {
126 lightRadius = lightRadius / (-1 * rogue.lightMultiplier + 1);
127 } else {
128 lightRadius *= rogue.lightMultiplier;
129 lightRadius = max(lightRadius, (rogue.lightMultiplier * 2 + 2) * FP_FACTOR);
130 }
131
132 if (player.status[STATUS_DARKNESS]) {
133 base_fraction = FP_FACTOR - player.status[STATUS_DARKNESS] * FP_FACTOR / player.maxStatus[STATUS_DARKNESS];
134 fraction = (base_fraction * base_fraction / FP_FACTOR) * base_fraction / FP_FACTOR;
135 //fraction = (double) pow(1.0 - (((double) player.status[STATUS_DARKNESS]) / player.maxStatus[STATUS_DARKNESS]), 3);
136 if (fraction < FP_FACTOR / 20) {
137 fraction = FP_FACTOR / 20;
138 }
139 lightRadius = lightRadius * fraction / FP_FACTOR;
140 } else {
141 fraction = FP_FACTOR;
142 }
143
144 if (lightRadius < 2 * FP_FACTOR) {
145 lightRadius = 2 * FP_FACTOR;
146 }
147
148 if (rogue.inWater && lightRadius > 3 * FP_FACTOR) {
149 lightRadius = max(lightRadius / 2, 3 * FP_FACTOR);
150 }
151
152 rogue.minersLight.radialFadeToPercent = 35 + (max(0, min(65, rogue.lightMultiplier * 5)) * fraction) / FP_FACTOR;
153 rogue.minersLight.lightRadius.upperBound = rogue.minersLight.lightRadius.lowerBound = clamp(lightRadius / FP_FACTOR, -30000, 30000);
154 }
155
updateDisplayDetail()156 void updateDisplayDetail() {
157 short i, j;
158
159 for (i = 0; i < DCOLS; i++) {
160 for (j = 0; j < DROWS; j++) {
161 if (tmap[i][j].light[0] < -10
162 && tmap[i][j].light[1] < -10
163 && tmap[i][j].light[2] < -10) {
164
165 displayDetail[i][j] = DV_DARK;
166 } else if (pmap[i][j].flags & IS_IN_SHADOW) {
167 displayDetail[i][j] = DV_UNLIT;
168 } else {
169 displayDetail[i][j] = DV_LIT;
170 }
171 }
172 }
173 }
174
backUpLighting(short lights[DCOLS][DROWS][3])175 void backUpLighting(short lights[DCOLS][DROWS][3]) {
176 short i, j, k;
177 for (i=0; i<DCOLS; i++) {
178 for (j=0; j<DROWS; j++) {
179 for (k=0; k<3; k++) {
180 lights[i][j][k] = tmap[i][j].light[k];
181 }
182 }
183 }
184 }
185
restoreLighting(short lights[DCOLS][DROWS][3])186 void restoreLighting(short lights[DCOLS][DROWS][3]) {
187 short i, j, k;
188 for (i=0; i<DCOLS; i++) {
189 for (j=0; j<DROWS; j++) {
190 for (k=0; k<3; k++) {
191 tmap[i][j].light[k] = lights[i][j][k];
192 }
193 }
194 }
195 }
196
recordOldLights()197 void recordOldLights() {
198 short i, j, k;
199 for (i = 0; i < DCOLS; i++) {
200 for (j = 0; j < DROWS; j++) {
201 for (k=0; k<3; k++) {
202 tmap[i][j].oldLight[k] = tmap[i][j].light[k];
203 }
204 }
205 }
206 }
207
updateLighting()208 void updateLighting() {
209 short i, j, k;
210 enum dungeonLayers layer;
211 enum tileType tile;
212
213 // Copy Light over oldLight
214 recordOldLights();
215
216 // and then zero out Light.
217 for (i = 0; i < DCOLS; i++) {
218 for (j = 0; j < DROWS; j++) {
219 for (k=0; k<3; k++) {
220 tmap[i][j].light[k] = 0;
221 }
222 pmap[i][j].flags |= IS_IN_SHADOW;
223 }
224 }
225
226 // Paint all glowing tiles.
227 for (i = 0; i < DCOLS; i++) {
228 for (j = 0; j < DROWS; j++) {
229 for (layer = 0; layer < NUMBER_TERRAIN_LAYERS; layer++) {
230 tile = pmap[i][j].layers[layer];
231 if (tileCatalog[tile].glowLight) {
232 paintLight(&(lightCatalog[tileCatalog[tile].glowLight]), i, j, false, false);
233 }
234 }
235 }
236 }
237
238 // Cycle through monsters and paint their lights:
239 boolean handledPlayer = false;
240 for (creatureIterator it = iterateCreatures(monsters); !handledPlayer || hasNextCreature(it);) {
241 creature *monst = !handledPlayer ? &player : nextCreature(&it);
242 handledPlayer = true;
243 if (monst->info.intrinsicLightType) {
244 paintLight(&lightCatalog[monst->info.intrinsicLightType], monst->xLoc, monst->yLoc, false, false);
245 }
246 if (monst->mutationIndex >= 0 && mutationCatalog[monst->mutationIndex].light != NO_LIGHT) {
247 paintLight(&lightCatalog[mutationCatalog[monst->mutationIndex].light], monst->xLoc, monst->yLoc, false, false);
248 }
249
250 if (monst->status[STATUS_BURNING] && !(monst->info.flags & MONST_FIERY)) {
251 paintLight(&lightCatalog[BURNING_CREATURE_LIGHT], monst->xLoc, monst->yLoc, false, false);
252 }
253
254 if (monsterRevealed(monst)) {
255 paintLight(&lightCatalog[TELEPATHY_LIGHT], monst->xLoc, monst->yLoc, false, true);
256 }
257 }
258
259 // Also paint telepathy lights for dormant monsters.
260 for (creatureIterator it = iterateCreatures(dormantMonsters); hasNextCreature(it);) {
261 creature *monst = nextCreature(&it);
262 if (monsterRevealed(monst)) {
263 paintLight(&lightCatalog[TELEPATHY_LIGHT], monst->xLoc, monst->yLoc, false, true);
264 }
265 }
266
267 updateDisplayDetail();
268
269 // Miner's light:
270 paintLight(&rogue.minersLight, player.xLoc, player.yLoc, true, true);
271
272 if (player.status[STATUS_INVISIBLE]) {
273 player.info.foreColor = &playerInvisibleColor;
274 } else if (playerInDarkness()) {
275 player.info.foreColor = &playerInDarknessColor;
276 } else if (pmap[player.xLoc][player.yLoc].flags & IS_IN_SHADOW) {
277 player.info.foreColor = &playerInShadowColor;
278 } else {
279 player.info.foreColor = &playerInLightColor;
280 }
281 }
282
playerInDarkness()283 boolean playerInDarkness() {
284 return (tmap[player.xLoc][player.yLoc].light[0] + 10 < minersLightColor.red
285 && tmap[player.xLoc][player.yLoc].light[1] + 10 < minersLightColor.green
286 && tmap[player.xLoc][player.yLoc].light[2] + 10 < minersLightColor.blue);
287 }
288
289 #define flarePrecision 1000
290
newFlare(lightSource * light,short x,short y,short changePerFrame,short limit)291 flare *newFlare(lightSource *light, short x, short y, short changePerFrame, short limit) {
292 flare *theFlare = malloc(sizeof(flare));
293 memset(theFlare, '\0', sizeof(flare));
294 theFlare->light = light;
295 theFlare->xLoc = x;
296 theFlare->yLoc = y;
297 theFlare->coeffChangeAmount = changePerFrame;
298 if (theFlare->coeffChangeAmount == 0) {
299 theFlare->coeffChangeAmount = 1; // no change would mean it lasts forever, which usually breaks things
300 }
301 theFlare->coeffLimit = limit;
302 theFlare->coeff = 100 * flarePrecision;
303 theFlare->turnNumber = rogue.absoluteTurnNumber;
304 return theFlare;
305 }
306
307 // Creates a new fading flare as described and sticks it into the stack so it will fire at the end of the turn.
createFlare(short x,short y,enum lightType lightIndex)308 void createFlare(short x, short y, enum lightType lightIndex) {
309 flare *theFlare;
310
311 theFlare = newFlare(&(lightCatalog[lightIndex]), x, y, -15, 0);
312
313 if (rogue.flareCount >= rogue.flareCapacity) {
314 rogue.flareCapacity += 10;
315 rogue.flares = realloc(rogue.flares, sizeof(flare *) * rogue.flareCapacity);
316 }
317 rogue.flares[rogue.flareCount] = theFlare;
318 rogue.flareCount++;
319 }
320
flareIsActive(flare * theFlare)321 boolean flareIsActive(flare *theFlare) {
322 const boolean increasing = (theFlare->coeffChangeAmount > 0);
323 boolean active = true;
324
325 if (theFlare->turnNumber > 0 && theFlare->turnNumber < rogue.absoluteTurnNumber - 1) {
326 active = false;
327 }
328 if (increasing) {
329 if ((short) (theFlare->coeff / flarePrecision) > theFlare->coeffLimit) {
330 active = false;
331 }
332 } else {
333 if ((short) (theFlare->coeff / flarePrecision) < theFlare->coeffLimit) {
334 active = false;
335 }
336 }
337 return active;
338 }
339
340 // Returns true if the flare is still active; false if it's not.
updateFlare(flare * theFlare)341 boolean updateFlare(flare *theFlare) {
342 if (!flareIsActive(theFlare)) {
343 return false;
344 }
345 theFlare->coeff += (theFlare->coeffChangeAmount) * flarePrecision / 10;
346 theFlare->coeffChangeAmount = theFlare->coeffChangeAmount * 12 / 10;
347 return flareIsActive(theFlare);
348 }
349
350 // Returns whether it overlaps with the field of view.
drawFlareFrame(flare * theFlare)351 boolean drawFlareFrame(flare *theFlare) {
352 boolean inView;
353 lightSource tempLight = *(theFlare->light);
354 color tempColor = *(tempLight.lightColor);
355
356 if (!flareIsActive(theFlare)) {
357 return false;
358 }
359 tempLight.lightRadius.lowerBound = ((long) tempLight.lightRadius.lowerBound) * theFlare->coeff / (flarePrecision * 100);
360 tempLight.lightRadius.upperBound = ((long) tempLight.lightRadius.upperBound) * theFlare->coeff / (flarePrecision * 100);
361 applyColorScalar(&tempColor, theFlare->coeff / flarePrecision);
362 tempLight.lightColor = &tempColor;
363 inView = paintLight(&tempLight, theFlare->xLoc, theFlare->yLoc, false, true);
364
365 return inView;
366 }
367
368 // Frees the flares as they expire.
animateFlares(flare ** flares,short count)369 void animateFlares(flare **flares, short count) {
370 short lights[DCOLS][DROWS][3];
371 boolean inView, fastForward, atLeastOneFlareStillActive;
372 short i; // i iterates through the flare list
373
374 brogueAssert(rogue.RNG == RNG_SUBSTANTIVE);
375
376 backUpLighting(lights);
377 fastForward = rogue.trueColorMode || rogue.playbackFastForward;
378
379 do {
380 inView = false;
381 atLeastOneFlareStillActive = false;
382 for (i = 0; i < count; i++) {
383 if (flares[i]) {
384 if (updateFlare(flares[i])) {
385 atLeastOneFlareStillActive = true;
386 if (drawFlareFrame(flares[i])) {
387 inView = true;
388 }
389 } else {
390 free(flares[i]);
391 flares[i] = NULL;
392 }
393 }
394 }
395 demoteVisibility();
396 updateFieldOfViewDisplay(false, true);
397 if (!fastForward && (inView || rogue.playbackOmniscience) && atLeastOneFlareStillActive) {
398 fastForward = pauseBrogue(10);
399 }
400 recordOldLights();
401 restoreLighting(lights);
402 } while (atLeastOneFlareStillActive);
403 updateFieldOfViewDisplay(false, true);
404 }
405
deleteAllFlares()406 void deleteAllFlares() {
407 short i;
408 for (i=0; i<rogue.flareCount; i++) {
409 free(rogue.flares[i]);
410 }
411 rogue.flareCount = 0;
412 }
413