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